Jetpack ComposeでAndroidのパーミッションをリクエストする場合、accompanistのrememberPermissionState
を利用するのが便利です。
パーミッションリクエストでは、ユーザーが「許可しない」を2回選択した場合、リクエストしてもダイアログが表示されず、即座に失敗してしまいます。ユーザーが「許可しない」と選択したのだからそれ以上要求すべきではないというのは分かるのですが、その場合はユーザに別の案内をする必要があるのに、これを直接判定する手段が提供されていません。これはJetpack Composeだからとか、accompanistの機能不足、ということではなく、Androidシステムが提供していません。
その回避策としてActivityResultContractを直接使う場合の方法は以下で解説しています。
ではrememberPermissionState
ではどうやれば良いのかというのが今回の話題です。(rememberPermissionState
は内部的にはActivityResultContractが使われています)
ダイアログが表示されなかったことの判定方法
そもそも、直接的な検出方法が提供されていないのにどうやって検出するか、ですが
まずはshouldShowRequestPermissionRationale
の戻り値(permissionState.status.shouldShowRationale
)の変化を観測します。
これは、パーミッションリクエスト時により詳細な説明をせよというフラグですが、ユーザーが何回「許可しない」を選択したかで変化します。
0回ならfalse、1回ならtrue、2回ならfalse、と変化します。
リクエスト前と後の変化をまとめると以下になります。
許可しない回数 | リクエスト前 | ダイアログの選択 | コールバック時 |
---|---|---|---|
0回 | false | キャンセル | false |
0回 | false | 許可しない | true |
1回 | true | キャンセル | true |
1回 | true | 許可しない | false |
2回 | false | ダイアログが表示されない | false |
すなわち、リクエスト前も後もfalseの場合、一度も「許可しない」を選択されていない時にダイアログがキャンセルされたか、ダイアログが表示されなかったか、の2択に絞ることができます。
2択のうち後者であることを判定するには、shouldShowRequestPermissionRationale
がtrueになったことがないと判定できれば確実ですが、それには永続化保存が必要になるため、あまりお手軽な方法とは言えません。
ここでは、ダイアログが表示されたユーザー操作が行われた場合と、ダイアログが表示されず即座に終了してしまった場合では、経過時間に大きな隔たりがある。と想定し、結果が返ってくるまでが一定時間以下である場合にダイアログが表示されなかったと判定することとします。
この方法はもちろん完璧ではなく、誤判定の可能性はありますが、表示されなかったと判定するほど短い時間しか表示されなかった場合なので、表示されなかった場合のダイアログが出てきてもそれほど違和感を覚えられないので十分かなと考えています。
rememberPermissionStateWrapper を作る
前述の方法で判定できるにしても、リクエストの度に実装するのは面倒なので、ダイアログが表示されなかった場合の判定機能を持ったラッパーメソッドを作ってみます
判定方法は決まっているのでその通り実装して以下のようになるかと思います。
ダイアログが表示されなかったと判定する時間として300msを設定していますが、何か根拠がある訳でありません。経験上このぐらいの時間で判定すればうまくいくというだけのことです。
private const val ENOUGH_DURATION = 300L
@Composable
fun rememberPermissionStateWrapper(
permission: String,
onPermissionResult: (granted: Boolean, failedToShowDialog: Boolean) -> Unit = { _, _ -> },
): PermissionState {
val activity = LocalActivity.current!!
var shouldShowRationaleBefore = false
var start: Long = 0
val permissionState = rememberPermissionState(permission) {
val elapsedEnoughTime = System.currentTimeMillis() - start > ENOUGH_DURATION
val shouldShowRationaleAfter = ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
val failedToShowDialog = !(shouldShowRationaleBefore || shouldShowRationaleAfter || elapsedEnoughTime)
onPermissionResult(it, failedToShowDialog)
}
return object : PermissionState {
override val permission: String = permissionState.permission
override val status: PermissionStatus = permissionState.status
override fun launchPermissionRequest() {
shouldShowRationaleBefore = permissionState.status.shouldShowRationale
start = System.currentTimeMillis()
permissionState.launchPermissionRequest()
}
}
}
rememberPermissionState
のコールバックはgrantedの引数一つですが、第二引数でダイアログ表示がなされなかったか表示されたかが渡されるようにしています。
以上です