ActivityResultContractsを使ったパーミッションリクエストを改善したい

activity:1.2.0、fragment:1.3.0からstartActivityResultやrequestPermissionがdeprecatedになりましたね。代替手段としてActivityResultContractsを使うようにしましょう。ただ、パーミッションのリクエスト方法にちょっと引っかかりがあって改善したいなと思いました。

※ここで書いている方法でも良いけど、パーミッションリクエストだけじゃないし、用意されているContractクラスが使えないのはチョットうまい方法ではないなと考え直したのが以下の記事です。

https://qiita.com/ryo_mm2d/items/da6dea5f8a3605b57352

# ActivityResultContractsを使ったパーミッションリクエスト

ActivityResultContractsを使ったパーミッションリクエストの方法をおさらいしましょう。

ActivityやFragmentのonCreateの前に順序が保証される形でregisterForActivityResultを呼び出し、ActivityResultLauncherを取得します。

パーミッション一つだけの場合はRequestPermissionを使いコールバックを登録します。

```kotlin
private val launcher =
    registerForActivityResult(RequestPermission()) {
         // 結果の処理
    }
```

リクエストではlaunchに引数としてパーミッションを指定します。

```kotlin
launcher.launch(Manifest.permission.CAMERA)
```

これで結果が、registerForActivityResultで指定したコールバックが呼び出されます。パーミッションは一つだけなので、獲得できた場合true、そうでなければfalseが引数として渡されます。

複数のパーミッションを同時にリクエストしたい場合は、RequestMultiplePermissionsを使いコールバックを登録します。

```kotlin
private val multipleLauncher =
    registerForActivityResult(RequestMultiplePermissions()) {
         // 結果の処理
    }
```

リクエストはlaunchに引数としてパーミッションの配列を指定します。

```kotlin
multipleLauncher.launch(arrayOf(
    Manifest.permission.CAMERA,
    Manifest.permission.ACCESS_FINE_LOCATION
))
```

これで結果が、registerForActivityResultで指定したコールバックが呼び出されます。パーミッション名をキーとして、獲得できたかどうかのBooleanが値として入ったMapが引数として渡されます。

リクエストコードの管理をする必要がなく、ActivityやFragmentのメソッドのオーバーライドが不要と、従来のリクエスト方法よりシンプルで扱いやすくなっています。

# 改善点

ちょっと引っかかる部分というのが、コールバックを登録してActivityResultLauncherを取得した時点で、対象となるパーミッションが決まっていないというところですね。
registerForActivityResultをコールしている箇所と、launchをコールしている箇所は分かれていて、launchでパーミッションを指定するため、コールバックが想定していないパーミッションをリクエストできてしまう自由度が残ってしまっています。

ようするに、以下のようにregisterForActivityResultをコールした時点で対象とするパーミッションを固定できるようになっているべきではないかと言うことです。

```kotlin
private val launcher = registerForActivityResult(
    RequestPermissionContracts(Manifest.permission.CAMERA)
) {
     // 結果の処理
}
```

```kotlin
private val launcher= registerForActivityResult(
    RequestMultiplePermissionsContracts(
        Manifest.permission.CAMERA,
        Manifest.permission.ACCESS_FINE_LOCATION
    )
) {
     // 結果の処理
}
```

launchでは渡すべき引数がないのでUnitにしておきましょうか。

```kotlin
launcher.launch(Unit)
```

# 改善クラス

RequestPermission, RequestMultiplePermissionsはfinalクラスなので拡張できません。ActivityResultContractのサブクラスとして作ります。ただし、ComponantActivityのActivityResultRegistryの実装でRequestMultiplePermissionsが直接使用されているため、この定数と同じ値を使うなど、内部実装に依存した実装をする必要があります。

単一パーミッション用

```kotlin:RequestPermissionContracts.kt
class RequestPermissionContracts(
    private val permission: String
) : ActivityResultContract<Unit, Boolean>() {

    override fun createIntent(context: Context, input: Unit?): Intent =
        Intent(ACTION_REQUEST_PERMISSIONS).putExtra(EXTRA_PERMISSIONS, arrayOf(permission))

    override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
        if (resultCode != AppCompatActivity.RESULT_OK) return false
        return intent
            ?.getIntArrayExtra(EXTRA_PERMISSION_GRANT_RESULTS)
            ?.getOrNull(0) == PackageManager.PERMISSION_GRANTED
    }

    override fun getSynchronousResult(
        context: Context, input: Unit?
    ): SynchronousResult<Boolean>? =
        if (permission.isGranted(context)) {
            SynchronousResult(true)
        } else {
            null
        }

    companion object {
        private const val ACTION_REQUEST_PERMISSIONS =
            RequestMultiplePermissions.ACTION_REQUEST_PERMISSIONS
        private const val EXTRA_PERMISSIONS =
            RequestMultiplePermissions.EXTRA_PERMISSIONS
        private const val EXTRA_PERMISSION_GRANT_RESULTS =
            RequestMultiplePermissions.EXTRA_PERMISSION_GRANT_RESULTS

        private fun String.isGranted(context: Context): Boolean =
            ContextCompat.checkSelfPermission(context, this) == PackageManager.PERMISSION_GRANTED
    }
}
```

複数パーミッション用

```kotlin:RequestMultiplePermissionsContracts.kt
class RequestMultiplePermissionsContracts(
    private vararg val permissions: String
) : ActivityResultContract<Unit, Map<String, Boolean>>() {

    override fun createIntent(context: Context, input: Unit?): Intent =
        Intent(ACTION_REQUEST_PERMISSIONS).putExtra(EXTRA_PERMISSIONS, permissions)

    override fun parseResult(resultCode: Int, intent: Intent?): Map<String, Boolean> {
        if (resultCode != Activity.RESULT_OK) return emptyMap()
        if (intent == null) return emptyMap()
        val permissions = intent.getStringArrayExtra(EXTRA_PERMISSIONS)
        val grantResults = intent.getIntArrayExtra(EXTRA_PERMISSION_GRANT_RESULTS)
        if (grantResults == null || permissions == null) return emptyMap()
        return permissions.indices.map {
            permissions[it] to (grantResults[it] == PackageManager.PERMISSION_GRANTED)
        }.toMap()
    }

    override fun getSynchronousResult(
        context: Context, input: Unit?
    ): SynchronousResult<Map<String, Boolean>>? {
        if (permissions.isEmpty()) return SynchronousResult(emptyMap())
        var allGranted = true
        val grantState = permissions.map {
            val granted = it.isGranted(context)
            if (!granted) allGranted = false
            it to granted
        }
        if (allGranted) return SynchronousResult(grantState.toMap())
        return null
    }

    companion object {
        private const val ACTION_REQUEST_PERMISSIONS =
            RequestMultiplePermissions.ACTION_REQUEST_PERMISSIONS
        private const val EXTRA_PERMISSIONS =
            RequestMultiplePermissions.EXTRA_PERMISSIONS
        private const val EXTRA_PERMISSION_GRANT_RESULTS =
            RequestMultiplePermissions.EXTRA_PERMISSION_GRANT_RESULTS

        private fun String.isGranted(context: Context): Boolean =
            ContextCompat.checkSelfPermission(context, this) == PackageManager.PERMISSION_GRANTED
    }
}
```

使い方は前述のように、registerForActivityResultの第一引数として使い、対象のパーミッションをコンストラクタで指定します。

```kotlin
private val launcher = registerForActivityResult(
    RequestPermissionContracts(Manifest.permission.CAMERA)
) {
     // 結果の処理
}
```

```kotlin
private val launcher= registerForActivityResult(
    RequestMultiplePermissionsContracts(
        Manifest.permission.CAMERA,
        Manifest.permission.ACCESS_FINE_LOCATION
    )
) {
     // 結果の処理
}
```

これで、コールバックとその処理するパーミッションを固定したActivityResultLauncherを取得することができます。

以上です。