PermissionsDispatcherによる権限管理

# はじめに
本稿では [PermissionsDispatcher](https://github.com/permissions-dispatcher/PermissionsDispatcher) による権限管理に関して、簡単な説明とサンプルアプリの提示を行います。

# Android の権限に関する包括的な情報
以下の情報を参照のこと：

- [App Permissions](https://developer.android.com/guide/topics/permissions/overview)
    - Android の権限に関する包括的な情報

# PermissionsDispatcher

## ◆ 概要
PermissionsDispatcher は、実行時の権限を扱うためのシンプルなアノテーションベースの API を提供します。

このライブラリは、権限が許可されているかどうかのチェック用コードを大量に記述することに伴う負担を軽減し、クリーンで安全なコードの保持に寄与します。

## ◆ 公式サイト
https://github.com/permissions-dispatcher/PermissionsDispatcher

# サンプルアプリ

## ◆ 概要
カメラ起動ボタンを押下すると、カメラの権限の必要な機能が呼び出されるサンプルアプリです。

以下に、概要図を示します：
![PermissionsDispatcher2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/564c3c79-f05c-28d0-2a60-b9a57b54e4c1.png)

## ◆ AndroidManifest.xml 
*AndroidManifest.xml* にて、権限の設定が必要になります。

```xml
<!--
  権限の付与。
  記述し忘れても、当該権限を利用している PermissionsDispatcher 関連コードは
  ビルド時にも実行時にもエラーとならず、自動的に権限申請が却下扱いとなるので要注意。
-->
<uses-permission android:name="android.permission.CAMERA" />
```

## ◆アノテーション設定

### ☆ @&#xFFF0;RuntimePermissions
- 必須のアノテーションです。

- Activity や Fragment に対して `@RuntimePermissions` アノテーションを付与することにより、PermissionsDispatcher による権限のハンドリングが有効になります。

```kotlin
@RuntimePermissions
class MainActivity : AppCompatActivity() {
}
```

### ☆ @&#xFFF0;NeedsPermission
- 必須のアノテーションです。

- `@NeedsPermission` アノテーションが付与されたメソッドに対しては、annotation processor によりメソッド名に `WithPermissionCheck` が付与されたラッパーメソッドが自動的に生成されます。

- 開発者は、オリジナルのメソッドではなく、ラッパーメソッドを呼び出します。

- `@NeedsPermission` アノテーションが付与されたメソッドは、ラッパーメソッド経由で権限のリクエストが許可された直後に PermissionsDispatcher により自動的に呼び出されます。

- private スコープは使用できません。

```kotlin
/**
 * カメラの権限を必要とする任意のメソッド。
 */
@NeedsPermission(Manifest.permission.CAMERA)
fun showCamera() {
    Toast.makeText(this, "showCamera()", Toast.LENGTH_SHORT).show()
}
```

### ☆ @&#xFFF0;OnShowRationale
- `@OnShowRationale` アノテーションが付与されたメソッドは、『今後表示しない』が設定されていない状態で権限のリクエストが発生した直後に PermissionsDispatcher により自動的に呼び出されます。

- 引数に、続行か中断を指示できる PermissionRequest を指定することができます。これを用いると、ダイアログに渡すことによりダイアログ側で続行か中断の指示を出すようなことが可能になります。

- 引数が指定されていない場合は、`proceed${NeedsPermissionMethodName}ProcessRequest` と `cancel${NeedsPermissionMethodName}ProcessRequest` メソッドが `@RuntimePermissions` アノテーションが付与された Activity や Fragment に生成され、これらのメソッドにより続行か中断の指示を出すことができます。

- private スコープは使用できません。

```kotlin
/**
 * 権限が必要な理由を表示する任意のメソッド。
 */
@OnShowRationale(Manifest.permission.CAMERA)
fun showRationaleForCamera(request: PermissionRequest) {
    showRationaleDialog(request)
}

/**
 * `@OnShowRationale` 経由で起動され、権限が必要な理由を表示して許可の是非を問うダイアログを表示するメソッド。
 */
private fun showRationaleDialog(request: PermissionRequest) {
    AlertDialog.Builder(this)
            .setPositiveButton("許可") { _, _ -> request.proceed() }
            .setNegativeButton("拒否") { _, _ -> request.cancel() }
            .setCancelable(false)
            .setMessage("カメラの権限が必要です")
            .show()
}
```

### ☆ @&#xFFF0;OnPermissionDenied
- `@OnPermissionDenied` アノテーションが付与されたメソッドは、『今後表示しない』が設定されていない状態で権限付与が拒否された場合に PermissionsDispatcher により自動的に呼び出されます。

- private スコープは使用できません。

```kotlin
/**
 * 権限が拒否された場合に呼び出される任意のメソッド
 */
@OnPermissionDenied(Manifest.permission.CAMERA)
fun onCameraPermissionDenied() {
    Toast.makeText(this, "onCameraPermissionDenied()", Toast.LENGTH_SHORT).show()
}
```


### ☆ @&#xFFF0;OnNeverAskAgain
- `@OnNeverAskAgain` アノテーションが付与されたメソッドは、『今後表示しない』が設定された状態で権限付与が拒否された場合に PermissionsDispatcher により自動的に呼び出されます。

- private スコープは使用できません。

```kotlin
/**
 * 『今後表示しない』が選択された場合に呼び出される任意のメソッド
 */
@OnNeverAskAgain(Manifest.permission.CAMERA)
fun onCameraNeverAskAgain() {
    Toast.makeText(this, "onCameraNeverAskAgain()", Toast.LENGTH_SHORT).show()
}
```

## ◆ 自動生成コードとのつなぎ込み

### ☆ ラッパーメソッド
`@NeedsPermission` アノテーションの付与されたメソッドは、権限チェック込みで呼び出すためのラッパーメソッドが自動生成されます。

ラッパーメソッドの名称は、オリジナルのメソッド名の末尾に `WithPermissionCheck` が付いたものになります。

```kotlin
/**
 * 画面中央の『カメラ起動』ボタンが押された時に呼び出されるメソッド。
 */
private fun onShowCameraButtonClick() {
    // showCamera() を呼ぶのではなく、自動生成されたラッパーメソッドの showCameraWithPermissionCheck() を呼んでいる点に注意。
    showCameraWithPermissionCheck()
}
```

### ☆ onRequestPermissionsResult()
[OnRequestPermissionsResultCallback#onRequestPermissionsResult()](https://developer.android.com/reference/kotlin/androidx/core/app/ActivityCompat.OnRequestPermissionsResultCallback#onrequestpermissionsresult) の呼び出しを、自動生成されたハンドリング用の extension function に委譲します。

```kotlin
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    // 自動生成された権限ハンドリング用のコードに処理を委譲するための extension function を呼び出す。
    onRequestPermissionsResult(requestCode, grantResults)
}
```

# 画面サンプル

## ◆ MainActivity
<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/555739a8-04b5-edff-94c5-5e875154e80a.png">

## ◆ showRationaleDialog()
権限が必要な理由を表示するためのダイアログです。

権限設定ダイアログに置き換わるものではなく、権限設定ダイアログの前に表示されるダイアログになります。
<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/ba94ef38-1ec7-93fd-a79f-15bd08ed1c14.png">

## ◆ 権限設定ダイアログ
システムにより自動的に表示されるダイアログです。

### ☆ 通常表示
<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/fbf66bf8-1343-db5b-55f5-e23532dfc110.png">

### ☆ 『今後表示しない』チェック時
**今後表示しない**をチェックした際には、**許可**が選択できないようになっています。[^1]
<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/325fe080-6ee3-6c16-e73b-525040d8c02d.png">

# おわりに
本稿では [PermissionsDispatcher](https://github.com/permissions-dispatcher/PermissionsDispatcher) による権限管理に関して、簡単な説明とサンプルアプリの提示を行いました。

サンプルの詳細については [Appendix](https://qiita.com/beyondseeker/items/1f964d011ee0a10d9376#appendix) を参照してください。

# Appendix
## ◆ サンプルアプリの GitHub リポジトリ
- [サンプルアプリの GitHub リポジトリ](https://github.com/beyondseeker/chrono0016)

## ◆ 主要コード
- [サンプルアプリの主要コードのみ差分表示するdiff](https://github.com/beyondseeker/chrono0016/commit/c333c543cafbb3d61286353c4ac837ee234118d4)

## ◆ MainActivityソース

```kotlin
package com.objectfanatics.chrono0016

import android.Manifest
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import permissions.dispatcher.*

// ・必須のアノテーションです。
// ・Activity もしくは Fragment に対して @RuntimePermissions アノテーションを付与することにより、
// 　PermissionsDispatcher による権限のハンドリングが有効になります。
@RuntimePermissions
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<View>(R.id.show_camera_button).setOnClickListener {
            onShowCameraButtonClick()
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        // 自動生成された権限ハンドリング用のコードに処理を委譲するための extension function を呼び出す。
        onRequestPermissionsResult(requestCode, grantResults)
    }

    /**
     * 画面中央の『カメラ起動』ボタンが押された時に呼び出されるメソッド。
     */
    private fun onShowCameraButtonClick() {
        // showCamera() を呼ぶのではなく、自動生成されたラッパーメソッドの showCameraWithPermissionCheck() を呼んでいる点に注意。
        showCameraWithPermissionCheck()
    }

    /**
     * カメラの権限を必要とする任意のメソッド。
     */
    // ・必須のアノテーションです。
    // ・`@NeedsPermission` アノテーションが付与されたメソッドに対しては、annotation processor により
    //   `WithPermissionCheck` が付与されたラッパーメソッドが自動的に生成され、開発者はそちらの
    // 　ラッパーメソッドを呼び出します。
    // ・ラッパーメソッド経由で権限のリクエストが許可された直後に PermissionsDispatcher により自動的に呼び出されます。
    // ・private スコープは使用できません。
    @NeedsPermission(Manifest.permission.CAMERA)
    fun showCamera() {
        Toast.makeText(this, "showCamera()", Toast.LENGTH_SHORT).show()
    }

    /**
     * 権限が必要な理由を表示する任意のメソッド。
     */
    // ・`@OnShowRationale` アノテーションが付与されたメソッドは、『今後表示しない』が設定されていない状態で
    // 　権限のリクエストが発生した直後に PermissionsDispatcher により自動的に呼び出されます。
    // ・引数に、続行か中断を指示できる PermissionRequest を指定することができます。これを用いると、
    // 　ダイアログに渡すことによりダイアログ側で続行か中断の指示を出すようなことが可能になります。
    // ・引数が指定されていない場合は、proceed${NeedsPermissionMethodName}ProcessRequest と cancel${NeedsPermissionMethodName}ProcessRequest
    // 　メソッドが @RuntimePermissions アノテーションが付与された Activity や Fragment に生成され、
    // 　これらのメソッドにより続行か中断の指示を出すことができます。
    // ・private スコープは使用できません。
    @OnShowRationale(Manifest.permission.CAMERA)
    fun showRationaleForCamera(request: PermissionRequest) {
        showRationaleDialog(request)
    }

    /**
     * 権限が拒否された場合に呼び出される任意のメソッド
     */
    // ・`@OnPermissionDenied` アノテーションが付与されたメソッドは、『今後表示しない』が設定されていない状態で
    // 　権限付与が拒否された場合に PermissionsDispatcher により自動的に呼び出されます。
    // ・private スコープは使用できません。
    @OnPermissionDenied(Manifest.permission.CAMERA)
    fun onCameraPermissionDenied() {
        Toast.makeText(this, "onCameraPermissionDenied()", Toast.LENGTH_SHORT).show()
    }

    /**
     * 『今後表示しない』が選択された場合に呼び出される任意のメソッド
     */
    // `@OnNeverAskAgain` アノテーションが付与されたメソッドは、『今後表示しない』が設定された状態で
    // 権限付与が拒否された場合に PermissionsDispatcher により自動的に呼び出されます。
    @OnNeverAskAgain(Manifest.permission.CAMERA)
    fun onCameraNeverAskAgain() {
        Toast.makeText(this, "onCameraNeverAskAgain()", Toast.LENGTH_SHORT).show()
    }

    /**
     * `@OnShowRationale` 経由で起動され、権限が必要な理由を表示して許可の是非を問うダイアログを表示するメソッド。
     */
    private fun showRationaleDialog(request: PermissionRequest) {
        AlertDialog.Builder(this)
                .setPositiveButton("許可") { _, _ -> request.proceed() }
                .setNegativeButton("拒否") { _, _ -> request.cancel() }
                .setCancelable(false)
                .setMessage("カメラの権限が必要です")
                .show()
    }
}
```


[^1]: 許可した際には今後表示されないので、当然と言えば当然ですね。