Runtime Permissionの取得(システムがリクエストを管理)

投稿日:  更新日:

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へ紐づいた機能を実行する時点で、アプリ上から取得を行います。

この「アプリ上から取得する」手順が、ドキュメントのワークフローに紹介されています。

ワークフローに従うと次のようになります。

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が許可される動作です。

1回目2回目以降
1回目の許可動作
初めPermissionは許可されていません。Permissionが申請されると、ダイアログが開き、ユーザに許可・拒否の判定を求めます。判定の結果はコールバックの引数に返ります。その結果をみて許可されていれば、Permissionに紐付けられた機能の実行を行います。
2回目以降の許可動作
Permissionが許可されている事を確認して、Permissionに紐付けられた機能の実行を行います。以降はこの繰り返しになります。

拒否のフロー

Permissionが拒否される動作です。

1回目2回目以降「今後表示しない」をチェック←チェック以降
1回目の拒否動作
初めPermissionは許可されていません。Permissionが申請されると、ダイアログが開き、ユーザに取得の許可・拒否の判定を求めます。判定の結果はコールバックの引数に返ります。その結果をみて許可されていなければ、コールバックを抜けて何もしません。
2回目以降の拒否動作
前回が拒否であると、shouldShowRequestPermissionRationale( )がtrueを返し、Permissionを取得する根拠の説明が求められます。ダイアログなどで説明を表示します。そのダイアログのアクションを使って、Permissionを申請します。その他は、1回目の動作と同じです。以降はこの繰り返しになります。
「今後表示しない」をチェック
「今後表示しない」がチェックされると、shouldShowRequestPermissionRationale( )がfalseになるので、設定アプリへ誘導が行われるようになります。
今後表示しない」をチェック以降
「今後表示しない」がチェックされると、shouldShowRequestPermissionRationale( )がfalseになるので、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アプリのダイアログが表示されることになります。

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も取得されます。

Permissionを含むPermission

スポンサーリンク

関連記事:

Androidは携帯端末のOSで、スマートフォンに適しています。 スマート(Smart)とは「賢い・洗練された」といった意味を持つ形容詞です。 Androidはこの「スマート」をユーザ自身の手で育てることが出来ます。 どのように育てるのか! それは世界中の開発者からリリースされているアプリをインストールして育てます。 より高機能なアプリ、より使い勝手の良いアプリを探してインストールすれば、あなたのスマートフォンはもっと「スマート」になるでしょう! しかし、アプリは良心的なものばかりではありません。中には悪意を持ったアプリも存在します。 悪意を持ったアプリから、ユーザを守る仕組みの1つがPermissionです。 ここまではユーザ目線でした。 逆にアプリの開発者目線で言えば、Permissionはアプリの安全性をユーザにアピールする手段でもありあます。 今回は、このPermissionについてまとめます。 ...
Install-time(Normal)Permissionの取得方法についてまとめます。 Permissionの詳細は「Permissionとその一覧」を参照してください。 ...
Runtime(Dangerous)Permissionの取得方法についてまとめます。 現在、Runtime Permissoinの取得方法は2つあります。 ここで紹介するのはRuntime Permission(API≧23)が登場した当初から存在している方法です。 この方法は、リクエスト(Permissionの申請)の結果を共用のコールバックAppCompatActivity#onRequestPermissionsResult( )で受け取ります。複数のリクエスト行うと、複数の結果を一つのコールバックで受けることになり、コールバック内でRequestCodeによる分岐が必要になります。 つまり、「開発者がリクエストを管理」しなければなりません。 Permissionの詳細は「Permissionとその一覧」を参照してください。 ...
パーミッションシステムの仕様変更がAPI≧30で行われています。  (1)1回だけアクセス許可  (2)“今後表示しない”の非表示  (3)アプリの休止 ここでは、「(1)1回だけアクセス許可」について説明します。 ...
パーミッションシステムの仕様変更がAPI≧30で行われています。  (1)1回だけアクセス許可  (2)“今後表示しない”の非表示  (3)アプリの休止 ここでは、「(2)“今後表示しない”の非表示」について説明します。 ...
パーミッションシステムの仕様変更がAPI≧30で行われています。  (1)1回だけアクセス許可  (2)“今後表示しない”の非表示  (3)アプリの休止 ここでは、「(3)アプリの休止」について説明します。 ...
「アプリの休止(App hibernation)」は発動までの期間が長いので、端末上で実際にその動作を確認しようとすると、待機時間が長くなってしまい、効率が悪いです。 ですので、テストでは「アプリの休止」を手動で発動させます。 今回は、この手動で発動させる方法を説明します。 ...
数ヶ月にわたってアプリが操作されなかった時に、アプリは休止状態(アプリの休止)になり、自動的にRuntime Permissionがリセット(削除)されます。 この機能はデフォルトで有効(On)です。 しかし、このPermissionのリセットを、良しとしないアプリが存在するかも知れません。 そのようなアプリのために、管理ページからユーザによって機能を無効(Off)にできます。 また、プログラム中から管理ページへユーザを誘導できます。 この「管理ページへユーザを誘導する方法」を紹介します。デベロッパーブログにも紹介されています。 ...
スポンサーリンク