Runtime(Dangerous)Permissionの取得方法についてまとめます。
現在、Runtime Permissoinの取得方法は2つあります。
ここで紹介するのはRuntime Permission(API≧23)が登場した当初から存在している方法です。
この方法は、リクエスト(Permissionの申請)の結果を共用のコールバックAppCompatActivity#onRequestPermissionsResult( )で受け取ります。複数のリクエスト行うと、複数の結果を一つのコールバックで受けることになり、コールバック内でRequestCodeによる分岐が必要になります。
つまり、「開発者がリクエストを管理」しなければなりません。
Permissionの詳細は「Permissionとその一覧」を参照してください。
Permissonの取得
AndroidManifestファイルへ宣言
Nromal Permissionと同様に、AndroidManifest.xmlへ必要なPermissionを列記する必要があります。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.myapp">
<!-- 以下はNormal Permission -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- 以下はDangerous Permission -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application ...>
...
</application>
</manifest>
これだけでは、必要なPermissionを宣言しただけで、取得は出来ていません。
取得のワークフロー
Permissionへ紐づいた機能を実行する時点で、アプリ上から取得を行います。
この「アプリ上から取得する」手順が、ドキュメントのワークフローに紹介されています。
ワークフローに従うと次のようになります。

private const val PERMISSION = Manifest.permission.ACCESS_FINE_LOCATION
private const val REQUEST_ID0 = 222
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
findViewById<Button>(R.id.btnStart).setOnClickListener {
val _PermissionState = ActivityCompat.checkSelfPermission(this, PERMISSION)
when {
_PermissionState == PackageManager.PERMISSION_GRANTED -> {
/* Permissionに紐づいた機能を実行 */
}
ActivityCompat.shouldShowRequestPermissionRationale(this, PERMISSION) -> {
AlertDialog.Builder(this).apply {
setTitle(R.string.permission_dialog_title)
setMessage(R.string.permission_dialog_text)
setPositiveButton("OK")
{ dialog, which ->
ActivityCompat.requestPermissions(
this@MainActivity, arrayOf(PERMISSION), REQUEST_ID0)
}
}.create().show()
}
else -> {
ActivityCompat.requestPermissions(this, arrayOf(PERMISSION), REQUEST_ID0)
}
}
}
}
override fun onRequestPermissionsResult( // コールバック
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
if(grantResults.isEmpty())
throw RuntimeException(getString(R.string.permission_exception_mesg))
when (requestCode) {
REQUEST_ID0 -> {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
/* Permissionに紐づいた機能を実行 */
} else {
if (! ActivityCompat.shouldShowRequestPermissionRationale(this, PERMISSION)) {
showPermissionSnackbar()
}
}
}
else -> {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
}
...
}
ポイント(1):Permissionの状態チェック
Permissionに紐づいた機能(関数またはメソッド)を実行する前に、状態(許可・拒否)のチェックが必要です。
上記の関数またはメソッドは@RequiresPermissionアノテーションが付加されています。チェックを行わない場合、構文チェックでエラーになります。
ActivityCompat#checkSelfPermission( )が状態を返します。
ポイント(2):Permissionの取得根拠を説明
Permissionを申請する前に、取得する根拠の説明が推奨されています。
前回が拒否だった場合、Permissionの必要性についての理解が不十分であると考えられるためです。
前回が拒否の時に、ActivityCompat#shouldShowRequestPermissionRationale( )はtrueを返します。
ポイント(3):コールバックに結果が返る
Permissionを申請するとダイアログが開き、ユーザに許可・拒否の判定を求めます。
その結果はコールバックの引数に返ります。
ActivityCompat#requestPermissions( )で申請します。
AppCompatActivity#onRequestPermissionsResult( )がコールバックです。
ポイント(4):設定アプリへ誘導
ユーザが何度も拒否している中で「次回から表示しない」を選択すると、拒否状態が固定化されてダイアログが開かなくなります。
これにより、ユーザとアプリのコミュニケーションが途切れて、ユーザが状況を理解できなくなる可能性があります。
従って、拒否の影響でアプリの機能が制限されていることをユーザに知らせます。
これはユーザの操作をブロックしない方法(例:Snackbar)で行います。
また、固定化された判定が変更できるように、Setteingsアプリへ誘導することが推奨されています。
ポイント(その他)
Permission関連の関数(メソッド)はActivityCompat配下のものを使用します。
API<23世代(全てInstall-time Permission)の端末と後方互換性を保つためです。
許可のフロー
Permissionが許可される動作です。


拒否のフロー
Permissionが拒否される動作です。




設定アプリへ誘導する方法
Permissionの拒否の影響で、アプリの機能が制限されていることを、Snackbarでユーザに知らせます。
そのSnackbarのアクションを使ってSettingアプリを起動しています。
fun showPermissionSnackbar() {
Snackbar.make(
findViewById(android.R.id.content),
R.string.permission_snackbar_text,
Snackbar.LENGTH_LONG
).apply {
setBackgroundTint(Color.LTGRAY)
setTextColor(Color.BLACK)
setActionTextColor(Color.RED)
setAction(R.string.permission_snackbar_action) {
val _intent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", packageName, null)
)
startActivity(_intent)
}
}.show()
}
Snackbarは一定時間の経過後に閉じます。よって、ユーザの操作をブロックしません。
Permission情報の所在
アプリが取得したPermission情報は次の場所に格納されています。
※API≧30で変更になっています。
API<30
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<runtime-permissions version="7" ...>
...
<pkg name="パッケージ名">
<item name="android.permission.ACCESS_FINE_LOCATION" granted="true" flags="0" />
<item name="android.permission.ACCESS_COARSE_LOCATION" granted="true" flags="0" />
</pkg>
...
</runtime-permissions>
API≧30
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<runtime-permissions version="8" ...>
...
<pkg name="パッケージ名">
<permission name="android.permission.ACCESS_FINE_LOCATION" granted="true" flags="301" />
<permission name="android.permission.ACCESS_COARSE_LOCATION" granted="true" flags="301" />
</pkg>
...
</runtime-permissions>
Permission情報の出力
アプリが取得したPermission情報はターミナルから adb shell dumpsys コマンドを使って出力できます。
> adb shell dumpsys package パッケージ名
...
Packages:
Package [パッケージ名] (xxxxxxx):
...
requested permissions:
android.permission.ACCESS_COARSE_LOCATION
android.permission.ACCESS_FINE_LOCATION
android.permission.INTERNET
android.permission.FOREGROUND_SERVICE
install permissions:
android.permission.FOREGROUND_SERVICE: granted=true
android.permission.INTERNET: granted=true
User 0: ...
gids=[3002, 3003]
runtime permissions:
android.permission.ACCESS_FINE_LOCATION: granted=true, flags=[...]
android.permission.ACCESS_COARSE_LOCATION: granted=true, flags=[...]
...
注意1:バックグラウンドで処理は進む
マイアプリがPermissionを申請すると、ダイアログが開きます。
このダイアログは、システムがPackageInstallerアプリに依頼して開かせたものです。
マイアプリとPackageInstallerアプリは異なるアプリです。
この結果、マイアプリ画面の上に、PackageInstallerアプリのダイアログが表示されることになります。

[ ダイアログ表示前 ] > adb shell 'dumpsys activity activities | grep -B 1 "Run #[0-9]*:"' TaskRecord{9279bc2 #106 A=マイアプリパッケージ名 U=0 StackId=1 sz=1} Run #0: ActivityRecord{78e2195 u0 マイアプリパッケージ名/.MainActivity t106} [ ダイアログ表示後 ] > adb shell 'dumpsys activity activities | grep -B 1 "Run #[0-9]*:"' TaskRecord{9279bc2 #106 A=マイアプリパッケージ名 U=0 StackId=1 sz=2} Run #1: ActivityRecord{d31e4d4 u0 com.google.android.packageinstaller/com.android.packageinstaller.permission.ui.GrantPermissionsActivity t106} Run #0: ActivityRecord{78e2195 u0 マイアプリパッケージ名/.MainActivity t106}
マイアプリはバックグラウンドへ遷移し、ActivityはonPause( )を処理します。ダイアログが閉じれば、再びフォアグラウンドへ遷移し、ActivityはonResume( )を処理します。
このように、ダイアログが表示されている最中も、マイアプリのActivityはバックグラウンドで処理を続けています。
注意2:Permissionを含むPermission
同一Group内において、Permissionを含むPermissionがあります。
例えば、次のようなPermissonです。
READ_EXTERNAL_STORAGE ⊂ WRITE_EXTERNAL_STORAGE
ACCESS_COARSE_LOCATION ⊂ ACCESS_FINE_LOCATION
右辺のPermissionを取得すると、おのずと左辺のPersmissionも取得されます。

関連記事:
