740
0
0

runtime permission APIの挙動と正しい使い方

Published at February 21, 2018 11:39 a.m.
Edited at January 3, 2019 2:09 p.m.

アプリの状態と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した状態という事がわかる。

java
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なのかどうかが分かる。
ただし、以下の二つを区別することができない。

  1. たった今dialogが出て、Don't ask againでDENYされたばかり
  2. もともと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.のほうが簡単)
    1. shouldShow=falseならOS設定を、trueならrequestPermissions()を行う。
      • onCreateで一度requestPermission()をしているので、初期状態じゃないことが保証されている。
      • こっちの方が簡単
    2. shouldShowの値を覚えておき、requestPermissions()を呼ぶ。onRequestPermissionsResult()内の値とどちらもfalseなら、OS設定を表示。
      • こっちは、あとから事前にpermissionを求めない実装に変えやすい
      • onCreateでのrequestPermissions()ではOS設定は出てほしくないので、onCreateと許可ボタンで、requestPermissionsのrequestCodeを変えておくこと

事前には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なら、

java
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
``