数ヶ月にわたってアプリが操作されなかった時に、アプリは休止状態(アプリの休止)になり、自動的にRuntime Permissionがリセット(削除)されます。
この機能はデフォルトで有効(On)です。
しかし、このPermissionのリセットを、良しとしないアプリが存在するかも知れません。
そのようなアプリのために、管理ページからユーザによって機能を無効(Off)にできます。
また、プログラム中から管理ページへユーザを誘導できます。
この「管理ページへユーザを誘導する方法」を紹介します。デベロッパーブログにも紹介されています。
休止の判定解除
自動的なPermissionのリセットは「アプリの休止」になることで行われます。
ですので、「アプリの休止」に成らないように、休止の判定を解除します。つまり、「アプリの休止」を無効(Off)にします。
管理ページからユーザの手によって切り替えが可能です。



管理ページの場所
管理ページの場所は端末で稼働しているAndroidのバージョンと、アプリがビルドされた時のtargetSDKの関係により異なります。
| 端末のOS | targetSDK | 休止の影響 | 管理ページ | ||
|---|---|---|---|---|---|
| ≧Android12 (API31) | ≧31 | Permissionの取得をリセット 一時データ(キャッシュ)のクリア | 「アプリ情報」 | ||
| Android11 (API30) | 30 | Permissionの取得をリセット | |||
| Android6~10 (API23~29) | 「Playプロテクト」 | ※ | |||
| ※休止の仕様をAPI<30へバックポート(移植)、Google Play Service+Play Storeで実現 | |||||
環境設定
管理ページを開くための関数・メソッド(後述)が用意されています。
この関数・メソッドを利用するために、次のライブラリをdependenciesへ追加します。
dependencies {
          :
    implementation "androidx.concurrent:concurrent-futures-ktx:1.1.0"
//  implementation "androidx.concurrent:concurrent-futures:1.1.0"
          :     ↑↑上記の2つのどちらか
}
concurrent-futuresはGuavaライブラリのListenableFutureをAndroid用に実装したものです。※Guava:Googleが開発しているJava向けのライブラリ群
ListenableFutureは非同期処理を効率的に記述するための関数です。非同期処理の結果をコールバックで返すことが出来ます。
休止の状態を確認
「アプリの休止」が無効(Off)の状態であれば、管理ページを開く意味はありません。無効にするために管理ページを開くからです。
ですので、先に現在の状態を確認します。
PackageManagerCompat#getUnusedAppRestrictionsStatus( )が確認するメソッドです。このメソッドは次のようなパラメータを返します。
| パラメータ | 休止の状態 | 概要 | 
|---|---|---|
| ERROR | 無効(Off) | 状態を取得できない ・targetSDK<30 ・ユーザロック中(パターン、PINなどにより) | 
| FEATURE_NOT_AVAILABLE | サポートされていない ・API23~29の端末でバックポートされた環境にない | |
| DISABLED | サポートされている | |
| API_30_BACKPORT | 有効(On) | サポートされている ・API23~29の端末でバックポートされた環境にある | 
| API_30 | サポートされている ・API30で導入 (Permissionの自動リセット) | |
| API_31 | サポートされている ・API31で導入 (Permissionの自動リセット、キャッシュのクリア) | |
| ※パラメータ:UnusedAppRestrictionsConstants.XXX ※API23~29はバックポートされた環境で「アプリの休止」を実現 | ||
以下の例は、有効(On)の時のみ管理ページを開く処理(Dialogの表示)を行っています。
                      :
            val future: ListenableFuture<Int> = getUnusedAppRestrictionsStatus(this)
            future.addListener({
                val _appRestrictionsStatus = future.get()
                when (_appRestrictionsStatus) {
                    ERROR -> { }
                    FEATURE_NOT_AVAILABLE -> { }
                    DISABLED -> { }
                    API_30_BACKPORT, API_30, API_31 -> {
                        showHibernationDialog(this@MainActivity)
                    }
                }
            }, ContextCompat.getMainExecutor(this@MainActivity)) // Mainスレッドで処理
			          :
ListenableFutureを利用して、状態の確認が非同期で行われている点に注意してください。
API23~29の端末ではGoogle Play Serviceが「アプリの休止」の動作を担っています。
ですので、状態を取得するために、Google Play Serviceとアプリ間の通信(コンポーネントの呼出⇒処理に時間がかかる)が必要になるためです。
管理ページを開く
突然、管理ページを開いても、ユーザは理由を理解できない可能性があります。
ですので、管理ページを開く前に無効化する理由をダイアログで表示し、説明します。
IntentCompat#createManageUnusedAppRestrictionsIntent( )が管理ページのIntentを作成してくれます。
後は、Intentを発行するだけで、管理ページが開きます。
    fun showHibernationDialog(context: Context) {
        AlertDialog.Builder(context).apply {
            setTitle(R.string.hibernation_dialog_title)
            setMessage(R.string.hibernation_dialog_text)
            setPositiveButton(R.string.hibernation_dialog_pos) { dialog, which ->
                val _intent = IntentCompat.createManageUnusedAppRestrictionsIntent(
                    context, packageName)
                startActivity(_intent)
            }
            setNegativeButton(R.string.hibernation_dialog_neg) { dialog, which ->
                dialog.dismiss()
            }
        }.create().show()
    }



※デベロッパーブログは「startActivityForResult( )を使う必要がある!」と書かれていますが、ここでは無視してstartActivity( )を用いました。ブログの発言の理由が不明ですし、そもそも、startActivityForResult( )は非推奨です。
関連記事:
