runtime permission APIの挙動と正しい使い方
アプリの状態とAPIの戻り値の関係
以下の表で、
- 初期状態とは、インストール直後などの以下の状態を表す。
- 一度もpermissionのdialogを出していない
- かつ、一度もOS設定でpermissionのON/OFFを変えていない
- shouldShow()とはshouldShowRequestPermissionRationale()のこと。
checkSelfPermission() | shouldShow() | requestPermission() | |
---|---|---|---|
初期状態(DENY) | false | false | dialog出る |
ALLOW | true | false | dialog出ない |
OS設定でDENYに変更 | false | true | dialog出る |
dialogでDon't ask again未チェックでDENYした | false | true | dialog出る |
dialogでDon't ask againチェックでDENYした | false | false | dialog出ない |
上の表からわかるように、初期状態(一番上)とDon't ask againでのDENY(一番下)は、APIの戻り値だけからは区別がつけられない(二つのAPIがどちらもfalseを返すので)。
区別をつけたい場合には、以下のように最初にdialogを出してしまえば、初期状態ではなくなるので、以後false-falseの場合には、Don't ask againでDENYした状態という事がわかる。
void onCreate(Bundle saveInstanceState) {
if (saveInstanceState == null) {
requestPermissions();
}
}
onRequestPermissionsResult内で、Don't ask againでDENYしたばかりか、もともとDon't ask againだったのかを知る方法
onRequetPermissionsResult()内でshouldShowを見ることで、Don't ask againでのDENYなのかどうかが分かる。
ただし、以下の二つを区別することができない。
- たった今dialogが出て、Don't ask againでDENYされたばかり
- もともとDon't ask againでDENYされていたため、dialogは出ていない。
web上の記事をみると、どちらの場合にもOS設定を開くための案内を表示する方法は見つけられる。が、個人的には、1.の場合には案内は出てほしくない。(今DENYしたばかりなので、案内を出すのは煩いと思う)
この二つを区別するには、requestPermissions()を呼ぶ直前のshoudShowの値も使うと良い。
requestPermissions()前のshouldShow | → | onRequestPermissionsResult内でのshouldShow | |
---|---|---|---|
初期状態 | false | → dialogでDENYした |
true |
DENY+shouldShow=true | true | → dialogでDENYした |
true |
DENY+shouldShow=true | true | → dialogでDENYした(Don't ask again) |
false |
DENY+shouldShow=false (もともとDon't ask again) |
false | → dialogは表示されない |
false |
どちらもfalseの時のみOS設定を開くための案内を出す、とするのが良いだろう。
ちなみに、permission dialogは別のシステムのプロセスのActivityなので、自アプリのプロセスがkillされる可能性が0ではない。requestPermissions()前のshouldShowの値は、onSaveInstanceStateで保存しておくこと。
(ちなみに)Don't ask againはいつ出るか
"Don't ask again"のチェックボックスは、初期状態の時だけは、表示されない。
permissionのON/OFFの変更をいつチェックすべきか
permissionは、以下の二つのタイミングでON/OFFが変更される
- requestPermission()で表示されるdialogでALLOW/DENYされたとき
- OS設定でON/OFFを変えられる
どちらの場合でもON/OFFされたことに応じて、アプリの動きを変えたい(ONになったら位置情報の取得開始、OFFになったら停止など)。
いつ、permissionのチェックをすべきか。
結論
- onResume()でチェックは必須。
- 場合によっては(処理を早めたい場合など)には、onStart()やonRequestPermissionsResult()でもチェックしても良い。
考察
- dialogでallow/denyされた場合
- onRequestPermissionsResultが呼ばれる
- dialog(Activity)が無くなり、自アプリのonResumeが直ぐに呼ばれる。
- dialogは透明なactivityなので、onStartは呼ばれない
- OS設定で変えられた場合
- permissionをOFFにすると(revokeすると)アプリのプロセスはkillされる。(よって、onCreateから呼ばれる)
ActivityManager: Killing 29759:jp.sstouch.jiriri.dev/u0a332 (adj 700): permissions revoked
- permissionをON(allow)したときには、プロセスそのまま。
- 通常はOS設定画面から切り替わるので、onStartから呼ばれる。
- 画面分割を使っている場合には、アプリのfocusが当たった際に、onResumeから呼ばれる(onStartは呼ばれない)
→onResumeでのチェックは必須。dialogでALLOW/DENYされたときもすぐにonResumeが呼ばれるので、onResumeだけチェックすればよい。
まとめ
以下の何れか。
画面をcreateしたときにdialogでpermissionを求める場合
- onCreateでrequestPermissions()でdialogを出す
- onCreateじゃなくても良いが、onCreate以外にする理由もないので
- onResumeで、
- permissionがALLOWなら必要な処理を開始(例えば、位置情報の取得を開始するとか)。DENYなら停止。
- DENYの場合には、ユーザーが後から許可できるように「許可ボタン」を表示。
- 処理を早く開始したいなら、onRequestPermissinsResult()とonStart()でも、onResumeでの処理を行っても良い(任意)
- 「許可ボタン」では、以下のどちらかの処理を行う(多分1.のほうが簡単)
- shouldShow=falseならOS設定を、trueならrequestPermissions()を行う。
- onCreateで一度requestPermission()をしているので、初期状態じゃないことが保証されている。
- こっちの方が簡単
- shouldShowの値を覚えておき、requestPermissions()を呼ぶ。onRequestPermissionsResult()内の値とどちらもfalseなら、OS設定を表示。
- こっちは、あとから事前にpermissionを求めない実装に変えやすい
- onCreateでのrequestPermissions()ではOS設定は出てほしくないので、onCreateと許可ボタンで、requestPermissionsのrequestCodeを変えておくこと
- shouldShow=falseならOS設定を、trueならrequestPermissions()を行う。
事前にはpermissionは求めず、機能ボタンを押して初めてrequestPermissions()する場合
- 例えば、「録音」ボタンを押すと10秒録音するとか
- 「録音」ボタン押下で、permissionが無い場合はrequestPermissions()を呼ぶ。
- 初期状態とDon't ask againを区別できないので、requestPermissions()を呼ばずにいきなりOS設定画面を出す、という事はできない
- requestPermissions()を呼ぶ前のALLOW/DENYと、shouldShowを覚えておく。
- onRequestPermissionsResult()で、
- DENYされた場合、requestPermissions()前のshouldShowと今のshouldShowがともにfalseなら、OS設定画面を開く
- それ以外の場合には、何もしなくてよい。
- permissionがdenyされたかどうかの判断には、checkPermissionを使うのと、grantResultを付か方法があるが、前者にしておくほうが良い。
- 後者だと、targetSdkが低い時に、grantResultとして空のの配列が来るっぽい。
- onResumeで、permissionの必要な処理を開始
- 処理を早くしたいなら、onRequestPermissionsResultやonStartでも、開始しても良い。
おまけ
requestPermission()の二重起動の防止
permissionのdialog表示中は、二重に表示しないような仕組みが入っている(100%防げるわけではないが)
Activityのソースコードで、onCreate時に、permissionリクエスト中だったかどうかのフラグであるmHasCurrentPermissionsRequestを復帰させている。
requestPermissions()では、mHasCurrentPermissionsRequesがtrueなら、
onRequestPermissionsResult(requestCode, new String[0], new int[0]);
return;
としていて、二重起動しようとしたときには、onRequestPermissionsResultがすぐに呼ばれるようになっている。(その後、dialogでALLOW/DENYすると、その結果がonRequestPermissionsResultにわたってくる)
mHasCurrentPermissionsRequesは、onSaveInstanceStateで保存している。ということは、onSaveInstanceState以後にrequestPermissionsを呼ぶと、フラグは保持されないので、その保護機能は働かない事がある。(dialogが二重に出ると、どちらのresultもonRequestPermissionsResultに配送されてくる)
permissionのdialogはどう実装されているか
答え: Activity。
startActivityForResultで、
com.google.android.packageinstaller/com.android.packageinstaller.permission.ui.GrantPermissionsActivity
が起動される。
結果は、dispatchActivityResult()で、
- whoがREQUEST_PERMISSIONS_WHO_PREFIXだったら、onRequestPermissionsResultを呼ぶ
- そうじゃないなら、onActivityResultを呼ぶ
となっている。
OS設定を開く方法
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
PermissionChecker
PermissionChecker.checkSelfPermission
``