AndroidのRuntime Permissionの対応にjavaの匿名クラスを使ってみた

# はじめに

AndroidのRuntime Permissionの対応にjavaの匿名クラスを使ってみました。

権限が必要な機能をいろいろな場所から呼び出したり、
違うシチュエーションで同じ権限を必要とする場合などに、
パラメタなどの保存/復元処理が不要になり、
便利に権限チェックができます。




# 背景

2018 年 11 月 1 日以降、Google Playにアプリを公開するには、
targetSdkVersionをAndroid 8.0（API レベル 26）以降に設定する必要があり、

注1: https://developer.android.com/distribute/best-practices/develop/target-sdk?hl=JA



Android 6.0（API レベル 23）以降では、
アプリのインストール時ではなく、アプリの実行時にパーミッションの確認が行われます。

注2: https://developer.android.com/training/permissions/requesting?hl=ja


Runtime Permissionの対応を行う場合、
requestPermissions()で権限を要求したあとの制御が
ActivityのonRequestPermissionsResult()メソッドに戻ってくるため、
権限が必要な機能を呼び出す際にパラメタをどこかに保存しておかなければ、
権限をもらったあとに処理を続行できません。


1. 権限が必要な機能を呼び出すごとにパラメタを保存して
2. onRequestPermissionsResultメソッドで保存したパラメタを復元して
3. さらに本来実行したい処理を続行させる

というのはとてもしんどそうでした。

ならば、権限が必要な処理をRunnableの匿名クラスとして保存しておいて、
権限が付与されたらActivityのonRequestPermissionsResult()メソッドから
そのRunnbaleを実行すればOKという発想で作ってみました。

# androidの権限の確認ダイアログのフロー

以下の記事を参考にして確認ダイアログのフローを実装しました。

https://techbooster.org/android/application/17223/

実装した権限の確認ダイアログのフローでは、
権限の確認ダイアログで拒否を繰り返した場合、
大雑把には以下のようになります。

1. 初回
    1. OSの権限確認ダイアログ表示
    2. 拒否を選択
2. 2回め
    1. ユーザへ権限がなんのために必要か説明するダイアログを表示
    2. OSの権限確認ダイアログ表示(今後は表示しないチェックボックスあり)
    3. 今後は表示しないチェックボックスをチェックして拒否
3. 今後は表示しない
    1. アプリの権限設定変更の案内ダイアログ表示

2-2の権限が拒否された場合に、2回め以降に追加でダイアログを表示するのは、
以下のガイドラインに従っています。

https://developer.android.com/training/permissions/requesting?hl=ja#perm-request

# コードの説明

サンプルコードは以下においてあります。

https://bitbucket.org/cnaos/runtimepermissionexample/src/master/

## 権限チェックの呼び出し元

以下が権限のチェックが必要な処理の呼び出し部分です。
ボタンを押したときのリスナーとして実装しています。

2つのボタンにそれぞれ別の機能を設定していますが、
両方ともカメラの機能をつかっていても、
一方はアイコン画像の変更、もう一方は写真投稿という
微妙に機能が異なるというシチュエーションです。

<img width="50%" src="https://bitbucket.org/cnaos/runtimepermissionexample/raw/master/screenshot/01_main.png"/>


```java
findViewById(R.id.button_change_icon).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //ボタンが押されたときのコールバック
        checkAppPermission(
                REQUEST_CAMERA_PERMISSION,  // arg1: ActivityのRequestCode
                Manifest.permission.CAMERA, // arg2: 必要な権限
                new Runnable() {
                    @Override
                    public void run() {
                        Log.d(TAG,"OP:change icon");
                        showLaunchCameraDialog("機能：アイコン変更","カメラで撮影した画像をアイコンに設定します。");
                    }
                }, // arg3: 権限が付与されているときに実行したい処理
                "アイコン画像を撮影するために、カメラを利用します" // arg4: 権限が拒否された場合に、2回め以降に追加で表示するダイアログの説明メッセージ
        );
    }
});
```



```java
findViewById(R.id.button_post_photo).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //ボタンが押されたときのコールバック
        checkAppPermission(
                REQUEST_CAMERA_PERMISSION,  // arg1: ActivityのRequestCode
                Manifest.permission.CAMERA, // arg2: 必要な権限
                new Runnable() {
                    @Override
                    public void run() {
                        // 権限が必要な、本来実行したい処理
                        Log.d(TAG,"OP:post photo");
                        showLaunchCameraDialog("機能：写真投稿","カメラで撮影した画像を投稿します。");
                    }
                }, // arg3: 権限が付与されているときに実行したい処理
                "写真投稿で使う画像を撮影するために、カメラを利用します"  // arg4: 権限が拒否された場合に、2回め以降に追加で表示するダイアログの説明メッセージ
        );
    }
});
```

checkAppPermission()メソッドが、今回実装した権限のチェック用メソッドです。

* 第1引数: ActivityのRequestCode
* 第2引数: 要求するPermission、
* 第3引数: 権限が必要な、実行したい処理、
* 第4引数: 権限が拒否された場合に、2回め以降に追加で表示するダイアログの説明メッセージ


## showLaunchCameraDialog

Runtime Permissionの動作確認のためのメソッドです。
ダイアログを表示したあとにカメラを起動しているだけです。

```java
private void showLaunchCameraDialog(String title, String message) {
    new AlertDialog.Builder(this)
            .setTitle(title)
            .setMessage(message)
            .setPositiveButton("OK",new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    // カメラを起動する
                    Intent intent = new Intent();
                    intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
                    intent.addCategory(Intent.CATEGORY_DEFAULT);
                    startActivityForResult(intent, 0);
                }
            })
            .setNegativeButton("Cancel", null)
            .show();
}
}
```



## checkAppPermission

今回実装した権限のチェック用メソッドです。
権限チェックの前半分です。
以下のことを行います。

* 権限が付与されているかどうかのチェック
* 権限が付与されている場合は、引数permissionGrantedOperationの処理の実行
* 権限が付与されたあとに実行する処理として、引数permissionGrantedOperationを保存。
* 権限が付与されていない場合に、権限を要求する理由についての説明ダイアログを出す
* 権限が付与されていない場合に権限を要求する

```java
private synchronized void checkAppPermission(
        final int requestCode,
        @NonNull final String checkPermission,
        @NonNull final Runnable permissionGrantedOperation,
        @NonNull final String permissionExplanationMessage){
    // A: 権限が付与されているか確認する。
    if(PermissionChecker.checkSelfPermission(
            this, checkPermission) == PackageManager.PERMISSION_GRANTED){
        // 権限が付与されているので実行する
        Log.d(TAG, "checkAppPermission: GRANTED:"+checkPermission+", requestCode="+requestCode);
        this.mPermissionGrantedOperation = null;
        permissionGrantedOperation.run();

        return;
    }

    // B: 権限が付与されたあとに処理が続行できるように、処理を保存しておく
    this.mPermissionGrantedOperation = permissionGrantedOperation;

    // C: ユーザに対して権限についての説明が必要かどうか確認する。
    if (ActivityCompat.shouldShowRequestPermissionRationale(this, checkPermission)) {
        Log.d(TAG, "checkAppPermission: shouldShowRequestPermissionRationale = true");
        // ユーザが一度権限を拒否しているので、権限についての説明のダイアログを表示する。
        new AlertDialog.Builder(this)
                .setTitle("パーミッションを要求する理由")
                .setMessage(permissionExplanationMessage)
                .setCancelable(false)
                .setNegativeButton(android.R.string.cancel, null)
                .setPositiveButton(android.R.string.ok,
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {
                                // 権限の説明ダイアログでOKが押された場合に、権限の要求を行う
                                Log.d(TAG, "checkAppPermission: requestPermissions(dialog): "+ checkPermission +", requestCode="+ requestCode);
                                ActivityCompat.requestPermissions(MainActivity.this,
                                        new String[]{checkPermission}, requestCode);
                            }
                        })
                .create()
                .show();
        return;
    }

    // D: 権限が付与されていないので、権限の要求を行う。
    Log.d(TAG, "checkAppPermission: requestPermissions: "+ checkPermission +", requestCode="+ requestCode);
    ActivityCompat.requestPermissions(this,
            new String[]{checkPermission},
            requestCode);

}
```

## onRequestPermissionsResult

OSの表示した権限の確認ダイアログの表示が終わると呼び出される
onRequestPermissionsResultメソッドです。

手抜きでpermissionは1個ずつしか要求しない想定です。
requestCodeで処理を分岐させて、processRequestPermissionResultメソッドに
完全に権限が拒否された場合のメッセージを渡しているだけです。

```java
public void onRequestPermissionsResult(
        int requestCode,
        @NonNull String[] permissions,
        @NonNull int[] grantResults) {

    if( grantResults.length == 0){
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        return;
    }

    if( requestCode != REQUEST_CAMERA_PERMISSION){
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        return;
    }

    switch(requestCode){
        case REQUEST_CAMERA_PERMISSION:
        {
            processRequestPermissionResult(
                    requestCode,
                    Manifest.permission.CAMERA,
                    grantResults[0],
                    "アプリ情報＞許可でカメラへのアクセスを許可してください"
            );
            return;
        }
    }
}
```

## processRequestPermissionResult

権限チェックの後ろ半分、
onRequestPermissionsResultメソッドから呼び出されるメソッドです。

```java
public void processRequestPermissionResult(
        final int requestCode,
        @NonNull final String requestedPermission,
        final int grantResult,
        @NonNull final String deniedDialogMessage) {
    // A: OSの権限確認ダイアログで、権限が付与されたか確認する。
    if ( grantResult == PackageManager.PERMISSION_GRANTED) {
        Log.d(TAG, "processRequestPermissionResult: GRANTED:" + requestedPermission + ", requestCode=" + requestCode);
        // 権限が付与されたので処理を実施する。
        executePermissionGrantedOperation();
        return;
    }

    // B: 権限が付与されてない。
    Log.d(TAG, "processRequestPermissionResult: DENIED:"+ requestedPermission +", requestCode="+requestCode);
    if ( ! ActivityCompat.shouldShowRequestPermissionRationale(this,
            requestedPermission)){
        // B-2: 今後表示しないが選択されているので、案内を出す
        Log.d(TAG, "processRequestPermissionResult: show app settings guide");
        new AlertDialog.Builder(this)
                .setMessage(deniedDialogMessage)
                .setNegativeButton("やめる",null)
                .setPositiveButton("設定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        openAppSettings();
                    }
                })
                .create()
                .show();
    }
}
```

## executePermissionGrantedOperation

保存してある権限が必要となる処理を実行します。
気休め程度にsynchronizedをつけています。

```java
private synchronized void executePermissionGrantedOperation() {
    Log.d(TAG,"executePermissionGrantedOperation");
    final Runnable tmpPermissionGrantedOperation = this.mPermissionGrantedOperation;
    if( tmpPermissionGrantedOperation == null ){
        Log.d(TAG,"executePermissionGrantedOperation: mPermissionGrantedOperation is null.");
        return;
    }
    this.mPermissionGrantedOperation = null;
    tmpPermissionGrantedOperation.run();
}
```