ラインタイムパーミッションのリクエストは伝統的に requestPermissions()
~ onRequestPermissionsResult()
で行われていましたが、この方法は非推奨となり、ActivityResultContractを使うことが推奨されていますね。
Activity/FragmentのコンストラクタもしくはonCreateにて
private val launcher = registerForActivityResult(RequestPermission()) {
if (it) {
// 許諾得られた
} else {
// 許諾得られなかった
}
}
として、コールバックを定義したActivityResultLauncherを登録し、以下のようにリクエストするパーミッションを引数に指定し、リクエストします。
launcher.launch(Manifest.permission.XXXXX)
単純なリクエストするだけであれば良いのですが、すこし使いにくい感があります。
ランタイムパーミッションのリクエストは2回ユーザーに拒否されたあとは、何も表示されず失敗しますが、そのケアは別途自前で行う必要があります。
ActivityResultLauncherとパーミッションを紐付ける
registerForActivityResult()
では、コールバックを実装する段階で、対象パーミッションが決まっておらず、リクエスト時にパーミッションを指定するスタイルのため、チョット使いにくい感がありますね。
スコープがActivityやFragmentに閉じるためそれほど広くはないですが、コールバックが想定しているパーミッションと異なるパーミッションをリクエストしてしまうリスクが出てくるので、リクエストするパーミッションとそれに対するコールバックという形で紐付けて定義したくなります。
ということで、紐付けできるようにしちゃいましょう。
以下のようにregisterForPermissionRequest
をラップする仕組みを作ると
interface PermissionRequestLauncher {
fun launch()
}
fun ComponentActivity.registerForPermissionRequest(
permission: String,
callback: (granted: Boolean) -> Unit,
): PermissionRequestLauncher =
PermissionRequestLauncherImpl(permission, registerForActivityResult(RequestPermission()) { granted ->
callback(granted)
})
fun Fragment.registerForPermissionRequest(
permission: String,
callback: (granted: Boolean) -> Unit,
): PermissionRequestLauncher =
PermissionRequestLauncherImpl(permission, registerForActivityResult(RequestPermission()) { granted ->
callback(granted)
})
class PermissionRequestLauncherImpl(
private val permission: String,
private val launcher: ActivityResultLauncher<String>,
) : PermissionRequestLauncher {
override fun launch() {
launcher.launch(permission)
}
}
以下のように、定義時にリクエストするパーミッション、それに対するコールバックという形で定義できるようになります。
private val launcher = registerForPermissionRequest(Manifest.permission.XXXXX) {
if (it) {
// 許諾得られた
} else {
// 許諾得られなかった
}
}
リクエスト時は引数なしで呼び出すだけです。
launcher.launch()
システムダイアログが表示されたかどうかを判定する
ランタイムパーミッションをリクエストして、ユーザーに2回「許可しない」を選択されると、以降ダイアログが表示されず、即座にコールバックが呼び出されます。
表示されなかった場合は、ユーザーにシステム設定で許可してもらう案内などを出す必要があります。一方、システムダイアログが表示された場合に再度許諾依頼を出すのはユーザーの心証を損ねるので避けたいところです。
しかし、この状態を直接検出する方法は提供されていません。
shouldShowRequestPermissionRationaleによる判定
「許可しない」を選択した場合に変化する値として、
ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.XXXXX)
の戻り値がありますね。パーミッションリクエストの根拠を示すべきかどうか、というメソッドですね。
1回目リクエストして、「許可しない」を選択されたあと、もう一度リクエストするときは、根拠を丁寧に説明してリクエストするべきという意味で用意されているメソッドですね。
このメソッドの戻り値は、以下のように変化します
「許可しない」の回数 | shouldShowRequestPermissionRationaleの値 |
---|---|
0回 | false |
1回 | true |
2回 | false |
システムダイアログの選択で許可されればそこで完了ですが、許可されないパターンとして、「許可しない」を選択した場合と、ダイアログ外部をタップするなどで「キャンセル」する場合の2パターンを考える必要があります。
ダイアログの選択内容と、パーミッションリクエスト前とコールバック時の変化を整理すると以下のようになります。
許可しない回数 | リクエスト前 | ダイアログの選択 | コールバック時 |
---|---|---|---|
0回 | false | キャンセル | false |
0回 | false | 許可しない | true |
1回 | true | キャンセル | true |
1回 | true | 許可しない | false |
2回 | false | ダイアログが表示されない | false |
リクエスト前後でshouldShowRequestPermissionRationale()
をコールし、どちらかの戻り値がtrueならシステムダイアログが表示されたと判定できます。
問題は、どちらもfalseの場合で、これは、ダイアログが表示されたがキャンセルされた。もしくは、システムダイアログが表示されなかった。ということで、ここが絞り込めません。
時間による判定
shouldShowRequestPermissionRationale()
の戻り値では、ダイアログが表示されたがユーザー操作でキャンセルされた場合か、システムダイアログが表示されず即座にコールバックが返ってきたのかの判定ができません。
ただし、ユーザー操作が入っているため、完璧でないにしても時間を計測すれば判定できそうです。
即座にコールバックが返る場合も100msぐらいはかかるようなので、連打などでキャンセルされた場合、誤判定をしてしまう可能性が出てきてしまいますが、「表示されなかった」→「十分な時間表示されなかった」と割り切ればそこまで不自然ではないと思いたい。
ダイアログ表示判定を追加
shouldShowRequestPermissionRationale()
の戻り値と、経過時間を組み合わせてシステムダイアログが表示されたかどうかを判定する仕組みを組み込んでみます。
interface PermissionRequestLauncher {
fun launch()
}
fun ComponentActivity.registerForPermissionRequest(
permission: String,
callback: (granted: Boolean, succeedToShowDialog: Boolean) -> Unit,
): PermissionRequestLauncher =
PermissionRequestLauncherImpl({ this }, permission).also { launcher ->
launcher.launcher = registerForActivityResult(RequestPermission()) { granted ->
callback(granted, launcher.succeedToShowDialog())
}
}
fun Fragment.registerForPermissionRequest(
permission: String,
callback: (granted: Boolean, succeedToShowDialog: Boolean) -> Unit,
): PermissionRequestLauncher =
PermissionRequestLauncherImpl({ requireActivity() }, permission).also { launcher ->
launcher.launcher = registerForActivityResult(RequestPermission()) { granted ->
callback(granted, launcher.succeedToShowDialog())
}
}
class PermissionRequestLauncherImpl(
private val activitySupplier: () -> Activity,
private val permission: String,
) : PermissionRequestLauncher {
lateinit var launcher: ActivityResultLauncher<String>
private var shouldShowRationalBefore: Boolean = false
private var start: Long = 0L
fun succeedToShowDialog(): Boolean {
if (shouldShowRationalBefore) return true
if (System.currentTimeMillis() - start > ENOUGH_DURATION) return true
return ActivityCompat.shouldShowRequestPermissionRationale(activitySupplier(), permission)
}
override fun launch() {
start = System.currentTimeMillis()
shouldShowRationalBefore =
ActivityCompat.shouldShowRequestPermissionRationale(activitySupplier(), permission)
launcher.launch(permission)
}
companion object {
private const val ENOUGH_DURATION = 1000L
}
}
1秒以上経過していたら表示されたという判定をしています。
システムダイアログが表示されたのに、再度説明のダイアログが表示された、よりも、なんの説明もなく失敗した、の方を避けるべきと考え大きめの値を設定していますが、アプリのユースケースに併せて調整すれば良いと思います。
shouldShowRequestPermissionRationale()
の判定にはActivityのインスタンスが必要なのですが、Fragmentの場合はonAttachまでの間Activityにアクセスできないため、registerForPermissionRequest()
ではなく、launch()
のタイミングでrequireActivity()
をコールするようにラムダで渡しています。
微妙にモヤモヤするやり方なのでよりよい方法はないだろうかと悩んでいるところです。
とにもかくにも、上記のような仕組みを作っておくと、以下のように許諾が得られなかった場合にシステムダイアログが表示されたかどうかの判定も併せて行うことができるようになります。
private val launcher = registerForPermissionRequest(Manifest.permission.XXXXX) {
granted, succeedToShowDialog ->
if (granted) {
// 許諾得られた
} else if (succeedToShowDialog) {
// システムダイアログが表示されたが許諾得られなかった
} else {
// システムダイアログが表示されなかった
}
}
以上です。