2015年10月25日日曜日

Requesting Permissions at Run Time

Requesting Permissions at Run Time

AndroidのエミュレーターをAndroid 6.0 (API level 23、Marshmallow)にすると、mkdir()が動作しなくなった。
また、次のようなエラーも出るようになった。
Caused by: java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaProvider uri content://media/external/video/media from pid=2627, uid=10058 requires android.permission.READ_EXTERNAL_STORAGE, or grantUriPermission()

これは次の事情による。
Android 6.0 Changes > Runtime Permissions
Requesting Permissions at Run Time

この問題をとりあえず解決するには、そのエミュレータ(端末)の「設定」>アプリ>(開発中のアプリを選択)>Permissionsを選択し、必要とするpermissionにチェックを入れればよろしい。

でも、その程度で解決する話ではないんだよね。

6.0では実行時に、permissionの許諾を端末利用者から得ることになる。インストール時ではない。

6.0からは、従前のpermissionに対し、上位概念のpermissionが設けられた。
従前のpermisssionは、例えば、ファイル操作関係であれば次のようなものだ。
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
AndroidManifest.xmlファイルにある、お馴染みさんである。

これに対して、6.0では、上位概念のpermissionとして、例えば、「STORAGE」が設けられた。
STORAGEは、下位に、READ_EXTERNAL_STORAGEとWRITE_EXTERNAL_STORAGEの2個のpermissionを含んでいる。
この上位-下位の関係性はSystem Permissionsの表に掲示されている。上位概念は「Permission Group」という用語で説明されている。

上述の操作「設定」>アプリ>(開発中のアプリを選択)>Permissionsにおいて表示される文字列(コマンド)は、この上位概念のpermissionである。このpermissionに許可を与えれば、READ_EXTERNAL_STORAGEとWRITE_EXTERNAL_STORAGEの2個の許可を与えたことになる(アプリが、それらを使うのであれば)。

Javaのプログラミング上では、上述の「上位概念」(Permission Group)は登場しないし、書く必要はない。引き続き、従前のpermission用語を使えば良い。

1個の下位概念のpermissionの使用に対して端末利用者の許諾を得たのであれば、その上位概念の許諾を得たことになるので、配下の他の下位概念で許諾を得る必要はない。
上位概念単位で端末利用者に許可を得ることになっている。

どのpermissionで対応すべきかは、上記のように、エラーメッセージの中に記されている。

端末利用者に許諾を得るタイミングは、permissionに関するメソッドを使用する前であれば、いつでも良い。一般的には、onCreate()において実施することになるのであろう。

留意すべきは、これは「許諾を得る」という次元の話である。
プログラムの実行時の制約は、下位概念のpermission毎に個別に異なる。例えば、readだけを使って上位概念の許諾を得ていた場合、writeの実行はできない。この場合、writeの実行時には地味なエラーになる。一旦許諾を得てしまえば、上記のような親切なエラーメッセージは何も表示されないので、何故エラーなのか悩むかもしれない。
readができるように許諾を得た後で、こそっと、その許諾に関するコードをwriteに変更したとしても、許諾関連には何も影響はない。そして、writeを実行できるようになる。

Javaのコードは、教科書Requesting Permissions at Run Timeに書かれてあるとおりである。
なお、build.gradleファイルには次のとおり追加する。
dependencies {
    compile 'com.android.support:support-v4:23.1.0'
}

    /*
    onCreate()から呼ばれる。
     */
    private boolean MyCheckPermission(){
        if(BuildConfig.DEBUG) Log.d(TAG, "MyCheckPermission()");
        int i;
        i = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
        if(i==PackageManager.PERMISSION_GRANTED){//既に許諾を得ている場合。
            //Android 6.0未満の場合、自動的にここに来る。バージョンによる振り分けをする必要はない。
            if(BuildConfig.DEBUG) Log.d(TAG, "MyCheckPermission() checkSelfPermission() is GRANTED");
            return true;//起動に必要な残りの処理を実行する。
        }
        //一度許諾を与えると、2度と、下記処理を行わない。
        else{//まだ許諾を得ていない場合
            boolean b;
            b = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE);
            if(b){//いきなり設定画面を表示するのではなく、permissionの必要性を(開発者の言葉で)説明する画面を表示する(任意)。
                //普通は、ここに来る。
                if(BuildConfig.DEBUG) Log.d(TAG, "MyCheckPermission() shouldShowRequestPermissionRationale is true");
                Intent it = new Intent(MainActivity.this, PermissionActivity.class);
                startActivityForResult(it, Nippon.ID_REQUEST_READ_EXTERNAL_STORAGE);
            }
            else{
                if(BuildConfig.DEBUG) Log.d(TAG, "MyCheckPermission() shouldShowRequestPermissionRationale is false");
                /*
                端末利用者が「二度とこの質問をしない」というチェックボックスにチェックしている場合、ここに来る。
                下記を実行することにより、過去の回答を自動で読み取る。画面に質問用画面を表示しない。
また、アンインストールをした後、再度インストールをした場合も、ここに来る。この場合、画面に質問用画面を表示する。
                */
                ActivityCompat.requestPermissions(
                    this,
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                    Nippon.ID_REQUEST_READ_EXTERNAL_STORAGE
                );
            }
            return false;//処理を、一旦、中断する。
        }
    }

    @Override
    protected void onActivityResult(int iRequest, int iResult, Intent it){
        if(BuildConfig.DEBUG) Log.d(TAG, "onActivityResult()");
        switch(iRequest){
        case Nippon.ID_REQUEST_READ_EXTERNAL_STORAGE:
            if(iResult==RESULT_OK){
                /*
                下記を実行するとAndroidシステム御用達の画面が表示され、READ_EXTERNAL_STORAGEを許可しますか? という趣旨の文言で聞かれる。
                端末利用者の選択結果はonRequestPermissionsResult()で受け取る。
                端末利用者が「二度とこの質問をしない」というチェックボックスにチェックすると、「拒否」しか選択できない表示になる(「承諾」を選択できない)
                アプリの運用途中で「拒否」&「2度と質問しない」をしてしまい、かつ、やっぱり気が変わって使いたいって思った場合、
                端末利用者は、手動で設定を変更しなければならない。
                */
                ActivityCompat.requestPermissions(
                    this,
                    //一度に、複数のpermissionを設定できるようにするため、配列形式になっている。
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                    Nippon.ID_REQUEST_READ_EXTERNAL_STORAGE
                );
            }
            else finish();//「戻る」キーで戻った場合
            break;
        }
        super.onActivityResult(iRequest, iResult, it);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
        case Nippon.ID_REQUEST_READ_EXTERNAL_STORAGE:
            if (grantResults.length > 0
            && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                continue_onCreate();
            }
            else finish();
        }
    }

2015年10月10日土曜日

google.android.gms.ads.AdActivity

google.android.gms.ads.AdActivity

google.android.gms.ads.AdActivityを使用しない場合(Admobの広告を使用しない場合)であっても、
\app\build\intermediates\manifests\full\debug\AndroidManifest.xmlファイルの下方には、次のように自動的に記載される。何故このような仕様にしているのかは問わない。

        <!-- Include the AdActivity and InAppPurchaseActivity configChanges and themes. -->
        <activity
            android:name="com.google.android.gms.ads.AdActivity"
            android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
            android:theme="@android:style/Theme.Translucent" />
        <activity
            android:name="com.google.android.gms.ads.purchase.InAppPurchaseActivity"
            android:theme="@style/Theme.IAPTheme" />

普段は人畜無害なので冬眠中の虫のようなものだ。
しかし、この記述内のandroid:configChangesの記載にエラーがあるという趣旨で、Build>Rebuild Projectの時、及びGenerate Signed APK...の時にエラーが発生した、

止むを得ないので、gms系のを全部削除した。
すると、新たなエラーが発生した。
gms系のエラーは真の原因では無く、闇の大ボスがいるようである。むむむ!

そこで、Proguardの設定ファイル内に次の行を書き込んでみた。
-dontwarn com.google.android.gms.**
すると、闇の大ボスは出なくなった。
gmsを使ってないのに!!!

Androidのプログラミングの難易度は結構高めです。

2015年9月6日日曜日

動画を連続再生させるアプリ

動画を連続再生させるアプリ

動画を連続再生させるアプリ「動画ん君」を開発・公開しました。
アプリ起動時に、自動的に、再生リストに基づき、動画の再生を開始できます。
お気軽にお使いください。

https://play.google.com/store/apps/details?id=jp.androyer.playmp4

2015年8月30日日曜日

launchMode="singleTop"に設定しているにも関わらず

launchMode="singleTop"に設定しているにも関わらず

端末利用者によるHomeKeyの押し下げにより、Activityは見えなくなるが、存在はする。
アプリのアイコンをクリックすると、当該Activityは、onCreate()ではなく、onStart()から始まり、表示される。
HomeKeyを押し下げてActivityが消えた時点で、
端末利用者がファイラーを使って、関連ファイルをクリックし、ImplicitIntentにより、当該Activityの起動が試みられると、
launchMode="singleTop"に設定しているにも関わらず。
既存のActivityがonNewIntent()でメッセージを受けるのではなく、
新たなActivityがonCreate()で起動してくる。
こうなると、2個のActivityが存在することになる。オブジェクトとしては別物である。
アプリのアイコンをクリックすると、古い方のActivityが動作・表示される。
こんな仕様だとは思わなかった。アプリを作りこんでから分かった。むむむ!

2015年8月28日金曜日

MediaControllerのバグかもしれない

MediaControllerのバグかもしれない

  1. VideoViewを使って、横位置FullScreenで動画を再生させる。
  2. MediaControllerを使って、pauseを手動で行う。動画は停止状態になる。
  3. 端末を縦位置にする。configuration changeを発生させる。
  4. Logが真っ赤になる。

真っ赤に流れるボクのLogCat
同じことを2回行うと、端末が反応しなくなり、再起動が必要になる。

E/WindowManager﹕ android.view.WindowLeaked: Activity jp.androyer.playmp4.MainActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView{2210feb8 V.E..... R....... 0,0-1280,117} that was originally added here
            at android.view.ViewRootImpl.<init>(ViewRootImpl.java:370)
            at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:248)
            at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
            at android.widget.MediaController.show(MediaController.java:349)
            at android.widget.MediaController.show(MediaController.java:306)
            at android.widget.VideoView.toggleMediaControlsVisiblity(VideoView.java:697)
            at android.widget.VideoView.onTouchEvent(VideoView.java:638)
            at android.view.View.dispatchTouchEvent(View.java:7767)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2230)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1931)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2230)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1931)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2230)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1931)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2230)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1931)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2230)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1931)
            at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2072)
            at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1518)
            at android.app.Activity.dispatchTouchEvent(Activity.java:2531)
            at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2020)
            at android.view.View.dispatchPointerEvent(View.java:8004)
            at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4136)
            at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:3990)
            at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3548)
            at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3598)
            at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3567)
            at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:3674)
            at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3575)
            at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:3731)
            at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3548)
            at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3598)
            at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3567)
            at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3575)
            at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3548)
            at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:5817)
            at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:5797)
            at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:5768)
            at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:5897)
            at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)
            at android.os.MessageQueue.nativePollOnce(Native Method)
            at android.os.MessageQueue.next(MessageQueue.java:138)
            at android.os.Looper.loop(Looper.java:131)
            at android.app.ActivityThread.main(ActivityThread.java:5257)
            at java.lang.reflect.Method.invokeNative(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:515)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:788)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:604)
            at dalvik.system.NativeStart.main(Native Method)

解答は次のサイトにあった。
Android MediaPlayer with MediaController: LogCat error “Activity has leaked window that was originally added here”

onSaveInstanceState()やonCreate(Bundle savedInstanceState)による作り込みを完成させてから、こんな現象が発生することを知った。むむむ!

2015年8月18日火曜日

常駐考察メモ

常駐考察メモ

私の出荷済アプリにおいては、参照カウンターを設けて、これがゼロになると、データベースを閉じる&アプリ終了するという方法を採用していた。
しかしながら、下記エラーが発生するという報告が利用者から来た。

Caused by: java.lang.IllegalStateException: Cannot perform this operation because the connection pool has been closed.

上記エラーへの対応方法をネットで調べると、「データベースを閉じるな」という方法が提案されている。
onDestroy()においてデータベースclose()しているにも関わらず、上記エラーが発生しているという状況から察するに、このclose()の後で、onStartCommand()がメッセージを受け取っているということが推測させる。
もはや、どうすることもできません。

●データベースを管理する。
●外部から不意のメッセージを受け取る
この2個の条件を満足させるためには、常駐化しかないかも。

参考:NotificationListenerServiceを実装させたアプリでは、NotificationListenerServiceは常駐している。また、類似アプリの振る舞いを観察しても、常駐させている。

NotificationListenerServiceに関する実験メモ

NotificationListenerServiceに関する実験メモ

【1】bindについて
NotificationListenerServiceは、Serviceであるため、まさにそのとおりに使えるのではないかという仮説を立てて、
1.Activityとbindで結ぶ
2.データベースの管理
3.NotificationListenerServiceとしての本来機能
の3個を、この順番で、実行をさせてみた。
上位2個までは順に順調に実行が完了していった。
本体の「設定」>「通知へのアクセス」>本アプリの選択したあと、通知(Notification)を受け取るようにしてみたところ、次のようなエラーが出た。

W/Parcel﹕ **** enforceInterface() expected 'android.os.IMessenger' but read 'android.service.notification.INotificationListener' W/Binder﹕ Caught a RuntimeException from the binder stub implementation. java.lang.SecurityException: Binder invocation to an incorrect interface at android.os.Parcel.nativeEnforceInterface(Native Method) at android.os.Parcel.enforceInterface(Parcel.java:451) at android.os.IMessenger$Stub.onTransact(IMessenger.java:48) at android.os.Binder.execTransact(Binder.java:404) at dalvik.system.NativeStart.run(Native Method)

残念なことに、私には詳細を調べる能力や気力はありません。
NotificationListenerServiceは、本体設定>「通知へのアクセス」の設定が行われた直後に、本来機能として「何か」とbindします。
しかしながら、上記実験プログラムでは、NotificationListenerServiceは、一番最初にActivityとbindしてしまっているため、上記エラーが発生したものと推測します。

推測ですが、NotificationListenerServiceは、「Avtivityとのbind」との共存はできないのかもです。

【2】他のアプリからのIntent
他のアプリからIntentで、NotificationListenerServiceにアクセスしようとすると、クラッシュする。
6736-6736/jp.androyer.vivitovib E/AndroidRuntime﹕ FATAL EXCEPTION: main
    Process: jp.androyer.vivitovib, PID: 6736
    java.lang.SecurityException: Not allowed to start service Intent { flg=0x20000000 cmp=jp.Androyer.NotificationLauncher/.NService (has extras) } without permission android.permission.BIND_NOTIFICATION_LISTENER_SERVICE
            at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1571)
            at android.app.ContextImpl.startService(ContextImpl.java:1548)
            at android.content.ContextWrapper.startService(ContextWrapper.java:495)
            at jp.androyer.vivitovib.Nippon$exportList.onPostExecute(Nippon.java:651)
            at jp.androyer.vivitovib.Nippon$exportList.onPostExecute(Nippon.java:638)
            at android.os.AsyncTask.finish(AsyncTask.java:632)
            at android.os.AsyncTask.access$600(AsyncTask.java:177)
            at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:645)
            at android.os.Handler.dispatchMessage(Handler.java:102)
            at android.os.Looper.loop(Looper.java:149)
            at android.app.ActivityThread.main(ActivityThread.java:5257)
            at java.lang.reflect.Method.invokeNative(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:515)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:788)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:604)
            at dalvik.system.NativeStart.main(Native Method)

クラッシュを引き起こさせるとは、とても思い罪を犯したということですね。