Runtime(Dangerous)Permissionの取得方法についてまとめます。
現在、Runtime Permissoinの取得方法は2つあります。
ここで紹介するのは、ActivityResultContracts(※)を使う方法です。
この方法は、リクエスト(Permissionの申請)と結果を受け取る専用のコールバックが1対1に対応しています。ですので、複数のリクエストを行っても、RequestCodeによる分岐処理は必要ありません。システムが結果を各コールバックへ割り振ってくれます。
つまり、「システムがリクエストを管理」してくれます。
ドキュメントで推奨されている方法です。
Permissionの詳細は「Permissionとその一覧」を参照してください。
※ライブラリandroidx.activity:activity≧1.20が必要
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
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val requestPermissionLauncher = registerForActivityResult(RequestPermission())
        { isGranted: Boolean ->  // コールバック(ActivityResultCallback#onActivityResult)
            if (isGranted) {
                /* Permissionに紐づいた(許可された)機能を実行 */
            } else {
                if (! ActivityCompat.shouldShowRequestPermissionRationale(this, PERMISSION)) {
                    showPermissionSnackbar()
                }
            }
        }
        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 ->
                            requestPermissionLauncher.launch(PERMISSION)
                        }
                    }.create().show()
                }
                else -> {
                    requestPermissionLauncher.launch(PERMISSION)
                }
            }
        }
    }
	
	...
}
ポイント(1):Permissionの状態チェック
Permissionに紐づいた機能(関数またはメソッド)を実行する前に、状態(許可・拒否)のチェックが必要です。
上記の関数またはメソッドは@RequiresPermissionアノテーションが付加されています。チェックを行わない場合、構文チェックでエラーになります。
ActivityCompat#checkSelfPermission( )が状態を返します。
ポイント(2):Permissionの取得根拠を説明
Permissionを申請する前に、取得する根拠の説明が推奨されています。
前回が拒否だった場合、Permissionの必要性についての理解が不十分であると考えられるためです。
前回が拒否の時に、ActivityCompat#shouldShowRequestPermissionRationale( )はtrueを返します。
ポイント(3):コールバックに結果が返る
Permissionを申請するとダイアログが開き、ユーザに許可・拒否の判定を求めます。
その結果はコールバックの引数に返ります。
ActivityCompat#requestPermissions( )で申請します。
ActivityResultCallback#onActivityResult( )がコールバックです。ラムダ式で表現されます。
ポイント(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も取得されます。

関連記事:
