いまさら Android 6.0 の Permissions at Run Time について
ここに書いてある内容が正しいという保証が一切持てないので、けっして鵜呑みにしないでください。
とりあえず公式ドキュメントのRequesting Permissions at Run Timeをざっくり読みました。
ランタイムパーミッションの概要
Andorid 6.0(API level23)以降では、ユーザーはインストール時ではなく、アプリの実行中にパーミッションを付与します。
このアプローチにより、ユーザーはアプリのインストールやアップデート時に権限を付与する必要がなくなったため、インストールプロセスがスリムになります。
それだけでなくユーザーはアプリの機能単位で、より詳細な制御を行うことができます。
例えば、カメラアプリが「カメラ」と「位置情報」の機能を要求する場合、「カメラ」へのアクセスを許可し、「位置情報」には権限を与えないことができます。
ユーザーはアプリの設定画面へ行くことで、いつでも権限の取り消しを行うことができます。
今までのパーミッションシステムには以下のような問題がありました。
- 今まではインストール・アップデート時に必要な権限が長々と並んで、ユーザーが許諾しないとアプリを入れられない仕組みだったけれど、インストール時点で「何のためにその権限を要求しているのか」ということは分かりづらかった。
- 説明がユーザーにとっては直感的ではない上に、ネタみたいなのもあった。
android.permission.BRICK
(あなたの端末を文鎮化する可能性があります)とか。 - すべてのパーミッション要求を許諾しないとインストールできない仕組みだったため、アプリのコア機能に不要な付加的なパーミッションも許諾する必要があった。
- 開発者でもないとその権限で何ができるのかは分からないので、危険なパーミッションがそれと知らずに紛れ込んでいる可能性があった。
ただし全ての問題が解決したわけではないですし、実装する側にとっては面倒きわまりない話です。これはそんな話です。
パーミッションのON/OFFができる粒度
パーミッションには膨大な数がありますが、それぞれの一つ一つについて、ON/OFFの制御をすることは できません 。
上記の記事が非常に参考になるのですが、パーミッショングループの単位でのみ設定が可能です。
パーミッショングループ
パーミッショングループについて補足しておくと、これはもともとパーミッションをカテゴライズしておくことで、インストール時に整理された状態で表示する機能でした。
古のインストール確認画面はこんな感じで、「現在地」や「ネットワーク通信」のように分かりやすく分類し、優先度の高いものが上に並ぶように並べ替え、そして本当に危険なパーミッションを「その他」に追いやって分かりにくくするための機能でした。
パーミッショングループ単位での指定の問題
「より詳細な制御」はできない
例えば「カレンダー」というグループ単位でONにすると、「カレンダーデータの読み込み(android.permission.READ_CALENDAR
)」と「カレンダーデータへの書き込み(android.permission.WRITE_CALENDAR
)」の両方を許諾したことになります。
アプリから「電話」を掛けられるようにONをすると、android.permission.READ_PHONE_STATE
が許諾されます。「電話の状態の読み取り」という牧歌的な名前に反して、電話番号から端末の一意識別子まで抜ける超危険なパーミッションのひとつです。
もちろん、マニフェストに宣言されていないパーミッションまでは取得できません。要求する権限は最小限にしましょう。
また、ユーザー視点としては、Permissions at Run Timeがあっても安心せずに、アプリが要求するパーミッションを見定める必要があります。
許諾状態は常に変わる可能性がある
このパーミッショングループの分類について、将来的に変更される可能性があります。
例えば「カレンダーの読み込み」と「書き込み」が分離されたとして、その場合は一度「カレンダー」としてパーミッションの許諾を得ていたとしても、再度パーミッションを取得する処理を入れる必要があります。
また、ユーザーは任意のタイミングで許諾の取り消しを行うことができます。このため、常に最新の許諾状態を取得する必要があります。
例えばカーナビアプリが定期的にバックグラウンドで位置情報を取得している場合、設定から位置情報の許諾を取り消された場合ってどうすればいいんでしょう?
フォアグラウンドのみのアプリならonResume()
での確認でよいのではと思うのですが、Android Nのマルチウインドウで設定を変えられたら?
許諾状態は常に変わる可能性を踏まえて、パーミッションの確認、要求ダイアログの表示などを自前で実装する必要があります。
パーミッショングループと危険性には相関がない
もともとAndroidのパーミッションにはProtectionLevel
という概念があり、個々のパーミッションについてnormal
やdangerous
などの格付けがありました。
この部分は正直確証がないのですが、Android 6.0ではこの仕組みを投げ捨てていているように思います。
Normal Permission
とDangerous Permission
に大分し、前者はユーザーの許諾を必要としないパーミッション、後者は必要とする(=パーミッショングループに定義されている)パーミッションというように再構築されているような…。
その結果、パーミッショングループに含まれない、dangerous
なパーミッションは現状では許可を取る必要なかったり、整合性を取るためにandroid.permission.INTERNET
がdangerous
からnormal
に格下されたりと不穏な感じがします。
どう実装すればいいのか
ここからが本題です。
Android Nが出る時代に、targetSDKを22にしたときの話をしてもしょうがないので、targetSDKを23以上にして開発するものとします。
また、requestPermissions()
を投げて、onRequestPermissionsResult()
で結果を得る、という基本的な部分は理解しているものとします。この部分についてはいくらでも情報があるので、特に困ることはないでしょう。
パーミッションを得ているかどうか知る方法
checkSelfPermission()
これは、PackageManager
のcheckPermission(String permName, String pkgName)
のシンタックスシュガーです。
checkPermission()
メソッドはAPI Lv1からあり、指定したパッケージのアプリがパーミッションを宣言しているかどうかを知ることができました。
Android 6.0ではこの意味合いが変わり、「パーミッションを宣言しており、かつユーザーに許可されているかどうか」に変わりました。
checkSelfPermission()
ではパッケージ名の指定が不要で、アプリ自身のパーミッションの取得状態を知ることができます。
ただし、checkSelfPermission()
はAPI Lv23以降にしかありません。そこで、互換ライブラリのContextCompat.checkSelfPermission()
を使うことができます。
実装は単純で、単に自分のパッケージ名を明示的に指定して、checkPermission()
を呼び出しているだけです。
このAPIを使うことで、
を知ることができます。
ただしまだrequestPermissions()
を呼んでいない場合には、PERMISSION_DENIED
が返ってきます。(targetSDK23以上の場合。22以下ではインストール時点で自動的にパーミッションの承諾が行われるので、ユーザーが明示的に設定を変えない限りはPERMISSION_GRANTED
が返って来る)
PermissionChecker
サポートライブラリには、PermissionChecker
というクラスもあり、checkSelfPermission()
など同名のメソッドが用意されています。
これが何のために存在するのかというと、Android 4.3のAppOpsという隠し機能 のためです。
ご存知の方はみんな知っての通り、Android 6.0で追加されたPermissions at Run Timeの原型とも言える仕組みは、Android 4.3の時点から隠し機能として搭載されていました。
恐らくですが、
- targetSdkを23未満でビルドしたアプリを、Android 6.0で動作させて、パーミッションを剥奪した場合
- このときはPermissions at Run Timeではなく、AppOpsで動作している?
- Android 6.0未満の端末で、App Opsを用いて、パーミッションを剥奪した場合
こうした状況では、PermissionChecker
がより適切です。App Opsによって剥奪されたパーミッションは、通常のrequestPermissions()
ではPERMISSION_GRANTED
が返却されますが、PermissionChecker
ではAppOpsManager
に問い合わせて、PERMISSION_DENIED_APP_OP
という結果を返してくれます。
ContextCompat.checkSelfPermission()
とPermissionChecker.checkSelfPermission()
のどちらを使うべきなのでしょうか。PermissionChecker
を推されている記事もありますが、公式ドキュメントではContextCompat
を使うことを推奨しています。
個人的には、今後はtargetSdk 23以降が基本となり、App Opsに対応しなければならない状況はほとんどなくなると思いますので、PermissionChecker
の存在は忘れてしまっても支障はないと思います。
パーミッションのリクエストを得る方法
requestPermissions()
例によって、以下の二つのメソッドがあります。
Activity#requestPermissions()
ActiivtyCompat#requestPermissions()
引数は取得したいパーミッションの配列(Manifest.permission
以下のString
定数)と、リクエストコードです。
オリジナルのrequestPermissions()
メソッド、およびコールバックとなるonRequestPermissionsResult()
は当然ながら、API level23以降のActivity
にしかありません。
ActiivtyCompat#requestPermissions()
は内部でバージョン判定を行ってくれるのので便利なのですが、両者の仕組みはちょっとだけ違います。
ほとんどの場合、AppCompatActivity
やv4パッケージの偽Fragment
を使っていると思うのですが、ActiivtyCompat#requestPermissions()
でリクエストした場合、FragmentActivity
が実装しているActivityCompat.OnRequestPermissionsResultCallback
インターフェースで結果が処理されます。
このとき、コールバック先がFragment
またはActivity
かの振り分けを16bitのシフト演算で行っています。これはFragmentActivity#startActivityForResult()
の原理をご存知でしたらお馴染みですね。ActiivtyCompat
版を使う場合には、リクエストコードの値が16bitを超えないようにする必要があります。
ActiivtyCompat
版では、 Android 6.0未満の端末でrequestPermissions()
を呼んだ場合、即座にonRequestPermissionsResult()
コールバックメソッドが呼び出される という違いがあります。
つまりAndroid 6.0未満がターゲットに入っていても、パーミッションリクエストが存在するものとして書いてしまえば、同じ処理で書けるのでは?
www.slideshare.net
PERMISSION_GRANTED
のパーミッションに対して、requestPermissions()
を行った場合に、何度でもリクエストのダイアログが表示されるという仕様らしく、この方法はダメみたいですね…。
バーミッションが拒絶された場合
Activity#shouldShowRequestPermissionRationale()
AppCompat#shouldShowRequestPermissionRationale()
もう2つある意味の説明は不要ですよね?
さて、一番良く分かんないメソッドがこれです。メソッドの意味としては「リクエストの根拠を示すべきか?」です。
とりあえず原文にあるサンプルコードを訳してみます。
// 以下、thisActivityが現在のActivityであるとする if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { // パーミッションを必要とする説明を表示する必要があるか? if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, Manifest.permission.READ_CONTACTS)) { // ユーザーへの説明を*非同期*で表示 ―決してブロックしてはいけない― // このスレッドはユーザーの応答を待っています! // ユーザーが説明を見た後、再度許可の要求を行ってください // 註)原文中のexpanationはexplanationのtypoとみなしました } else { // 何の説明も必要がありません、パーミッションの要求をすることができます ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.READ_CONTACTS}, MY_PERMISSIONS_REQUEST_READ_CONTACTS); // MY_PERMISSIONS_REQUEST_READ_CONTACTS はアプリが定義したint型の定数です // コールバックメソッドは要求の結果を得ることができます。 } }
まずパーミッションのチェックを行って、PERMISSION_DENIED
の場合に使うものみたいですね。
このメソッドについて理解するには、パーミッションダイアログの初回表示と2回目以降表示、そして「今後表示しない」が選択されたときの挙動を理解する必要があります。
分かりやすく表にしました
最初からこの表を用意すれば8000文字も書かなくて良かった気がしますね。
メソッド | Android 6.0未満 | 初回表示 | 2回目以降表示 | 今後表示しない |
---|---|---|---|---|
checkSelfPermission | GRANTED | DENIED | 前回選択に順ずる | DENIED |
requestPermissions | GRANTED | パーミッション許可 ダイアログ表示 |
パーミッション許可 ダイアログ表示 (「今後表示しない」付き) |
DENIED |
shouldShowRequest PermissionRationale |
false | false | true | false |
※全部AppCompat
でやっているものとします。
初回の問い合わせにおいて、checkSelfPermission()
はPERMISSION_DENIED
をshouldShowRequestPermissionRationale()
はfalse
を返します。そのままrequestPermissions()
を投げましょう。
一度リクエストが拒絶されたかどうかは、shouldShowRequestPermissionRationale()
を呼ぶことで知ることができます。
そこで、2度目のrequestPermissions()
の前に、なぜパーミッションを必要とするのか、その根拠を説明することができます。この処理は別途ダイアログを表示するなど、非同期で行う必要があります。もしユーザーが心変わりした場合だけ、requestPermissions()
を投げましょう。
「今後表示しない」が選択された場合、もはや弁解の余地はないのでshouldShowRequestPermissionRationale()
はfalse
を返しますし、requestPermissions()
を投げても即断でPERMISSION_DENIED
が返されます。
どうしても必要なパーミッションを拒絶された場合
もし「今後表示しない」が選ばれたパーミッションを、必ず要求するアプリの場合、どうすればいいのでしょうか?
アプリ設定を変更して貰うようなダイアログを表示するフローを用意するのが現実的です。しかしこれは「二度と表示するな」というユーザーの意思を尊重したものとはいえません。
shouldShowRequestPermissionRationale()
がtrue
を返したときに、「なぜこのアプリがパーミッションを要求するのか」という論理的な説明を行い、ユーザーに納得して貰うというのが最良の体験になるのではないでしょうか。
「今後表示しない」が選択されたことを知る方法
onRequestPermissionsResult()
コールバック内で、結果がPERMISSION_DENIED
かつ、shouldShowRequestPermissionRationale()
がfalse
を返した場合、「今後表示しない」が選択されたと判定することができます。
つまり、 必ずrequestPermissions()
を呼ぶ必要があります 。もし既に「今後表示しない」が選択されている場合に、そのパーミッションが必要なボタンをdisabled
にしておいたり、Snackbar
でさりげなく通知するといった気を利かせる方法はありません。
個人的にはこの仕様は、Permissions at Run Time の欠陥だと思っています。公式ドキュメントのサンプルコードだとこのケースについて何も考えてないので注意です。
ちなみにiOSの世界では
iOSでは、マイクや位置情報を利用するAPIの初回に利用許可を求めるダイアログをOSが自動的に表示します。
アプリケーションを再インストールしない限り、再度表示されることはありません。設定から変更して貰う必要があります。
requestPermissions()
のタイミングに注意しつつ、shouldShowRequestPermissionRationale()
は2回目以降ではtrue
を返す性質を利用すれば簡単にiOSと同じ挙動にできて便利ですね!
まとめ(rev2)
Permissions at Run Time は調べれば調べるほど闇が深い…。