Android11から導入されたAuto-reset permissionsを調べてみてわかったこと

はじめに

本記事はAndroid Advent Calendar 2020 14日目の記事です

qiita.com

本記事では、Android11から導入されたAuto-reset permissionsを調べてみてわかったことを書いていきます

Auto-reset permissionsとは?

長期間アプリを使わなかった場合、ユーザーがアプリに付与した機密情報に関わるPermission(権限)が自動的にリセットされる仕組みです Protection LevelがDangerousに分類されるPermissionが自動リセットの対象です つまり、すべてのPermissionが自動リセットの対象ではないということです

公式ドキュメントの説明はこのあたりか、ブログを読むといいかと思います

developer.android.com

developers-jp.googleblog.com

アプリに対してAuto-reset permissionsが適用されるかどうかは、ユーザの設定次第になります 設定アプリの各アプリケーション Permission画面下部に、長期間アプリを使わなかった場合にPermissionを自動的にリセットするかどうかの設定があります

f:id:operando:20201214060319p:plain

targetSDKVersionが30以上のアプリはこの設定値が初期状態で有効、targetSDKVersionが30未満のアプリこの設定値が初期状態で無効になります 公式ドキュメントでは、targetSdkVersionが30以上のアプリにAuto-reset permissionsが適用されるっぽい記載になっていますが、targetSDKVersionが30未満のアプリにも適用されることがあります

最終的には、ユーザの設定次第なので、targetSdkVersionが30以上のアプリでもユーザが設定値をOFFにすれば、Auto-reset permissionsは適用されなくなりますし、targetSDKVersionが30未満のアプリでもユーザが設定値をONにすれば、Auto-reset permissionsは適用されます

Auto-reset permissionsが実行され、Permissionが自動リセットされると通知が来ます 通知タップで遷移する画面でアプリをアンインストールできるのは便利ですね(自動リセットされる = 長期間使用していないアプリなのでアンインストールも検討してねってことだろう)

Auto-reset permissionsでPermissionが自動リセットされると通知が来る 通知タップで遷移する画面
f:id:operando:20201214071744p:plain f:id:operando:20201214071922p:plain

ここからは、Auto-reset permissionsの内部動作等について調べてわかったことなどを書いていきます

Android Code Searchを使って内部実装を調べる

Android Code Searchを使ってAuto-reset permissionsの内部実装を調べました

https://cs.android.com/

Auto-reset permissionsに関する実装はだいたいAutoRevokePermissionsクラスにまとまってます

https://cs.android.com/android/platform/superproject/+/master:packages/apps/PermissionController/src/com/android/permissioncontroller/permission/service/AutoRevokePermissions.kt

長期間アプリを使わなかった場合の「長期間」ってどれくらいの期間なのか

疑問に思ったのが、長期間アプリを使わなかった場合の「長期間」ってどれくらいの期間なの?ってところです

期間に関するしきい値の定数がありました デフォルトでは90日間らしいです

private val DEFAULT_UNUSED_THRESHOLD_MS =
        if (AUTO_REVOKE_ENABLED) DAYS.toMillis(90) else Long.MAX_VALUE

期間のしきい値を変更する

デフォルトってことは、つまり期間のしきい値を変更する方法が存在します 期間のしきい値にどの値を使うかを決めているコードを抜粋したのが以下です

fun getUnusedThresholdMs(context: Context) = when {
    DEBUG_OVERRIDE_THRESHOLDS -> SECONDS.toMillis(1)
    TeamfoodSettings.get(context) != null -> TeamfoodSettings.get(context)!!.unusedThresholdMs
    else -> DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS,
            PROPERTY_AUTO_REVOKE_UNUSED_THRESHOLD_MILLIS,
            DEFAULT_UNUSED_THRESHOLD_MS)
}

private data class TeamfoodSettings(
    val enabledForPreRApps: Boolean,
    val unusedThresholdMs: Long,
    val checkFrequencyMs: Long
) {
    companion object {
        private var cached: TeamfoodSettings? = null

        fun get(context: Context): TeamfoodSettings? {
            if (cached != null) return cached

            return Settings.Global.getString(context.contentResolver,
                "auto_revoke_parameters" /* Settings.Global.AUTO_REVOKE_PARAMETERS */)?.let { str ->

                if (DEBUG) {
                    DumpableLog.i(LOG_TAG, "Parsing teamfood setting value: $str")
                }
                str.split(",")
                    .mapNotNull {
                        val keyValue = it.split("=")
                        keyValue.getOrNull(0)?.let { key ->
                            key to keyValue.getOrNull(1)
                        }
                    }
                    .toMap()
                    .let { pairs ->
                        TeamfoodSettings(
                            enabledForPreRApps = pairs["enabledForPreRApps"] == "true",
                            unusedThresholdMs =
                                pairs["unusedThresholdMs"]?.toLongOrNull()
                                        ?: DEFAULT_UNUSED_THRESHOLD_MS,
                            checkFrequencyMs = pairs["checkFrequencyMs"]?.toLongOrNull()
                                    ?: DEFAULT_CHECK_FREQUENCY_MS)
                    }
            }.also {
                cached = it
                if (DEBUG) {
                    Log.i(LOG_TAG, "Parsed teamfood setting value: $it")
                }
            }
        }
    }

Settings.Global.AUTO_REVOKE_PARAMETERSから設定値を読み込んでいます(Settings.Global.AUTO_REVOKE_PARAMETERS はhide APIなので、通常のアプリケーションからは定数を参照できません)

Settings.Global.AUTO_REVOKE_PARAMETERSの設定値にunusedThresholdMsが存在すれば、未使用期間のしきい値にそれを使うような実装になっています

つまり、adb shellから以下のコマンドを実行することで、未使用期間のしきい値を変更できます

// 未使用期間のしきい値を1分に変更
adb shell settings put global auto_revoke_parameters enabledForPreRApps=false,unusedThresholdMs=60000,checkFrequencyMs=60000

// 設定できているかを確認
adb shell settings get global auto_revoke_parameters

adbではなくコードからSettings.Global.AUTO_REVOKE_PARAMETERSを設定しようと試みましたが、これはさすがにセキュリティ的に無理でした

// java.lang.SecurityException: Permission denial: writing to settings requires:android.permission.WRITE_SECURE_SETTINGS
Settings.Global.putString(contentResolver,
"auto_revoke_parameters",
"enabledForPreRApps=false,unusedThresholdMs=60000,checkFrequencyMs=60000")

設定は無理でしたが、設定値の読み込みはコードからできました

Settings.Global.getString(contentResolver,"auto_revoke_parameters")

Settings.Globalを設定する以外に、DeviceConfigを設定する方法もあります adb shellから以下のコマンドを実行することで、未使用期間のしきい値を変更できます

// 未使用期間のしきい値を1分に変更
adb shell device_config put permissions auto_revoke_unused_threshold_millis2 60000

// 設定できているかを確認
adb shell device_config get permissions auto_revoke_unused_threshold_millis2

どのくらいの周期でAuto-reset permissionsが実行されるのか

Auto-reset permissionsが実行される周期に関する定数がありました デフォルトでは15日間隔らしいです(広くね...?🤔)

private val DEFAULT_CHECK_FREQUENCY_MS = DAYS.toMillis(15)

内部ではJobSchedulerを使って定期実行しています BOOT_COMPLETEDでAutoRevokeOnBootReceiverを起動させて、そこでJobSchedulerを登録して、AutoRevokeServiceが定期的に実行されるようにしてます

Auto-reset permissionsの実行周期を変更する

こちらも期間のしきい値と同様、Settings.GlobalかDeviceConfigを設定することで変更できます

private fun getCheckFrequencyMs(context: Context) = when {
    TeamfoodSettings.get(context) != null -> TeamfoodSettings.get(context)!!.checkFrequencyMs
    else -> DeviceConfig.getLong(
            DeviceConfig.NAMESPACE_PERMISSIONS,
            PROPERTY_AUTO_REVOKE_CHECK_FREQUENCY_MILLIS,
            DEFAULT_CHECK_FREQUENCY_MS)
}

変更する際の注意点として、JobSchedulerの関係上 実行周期を15分以下にできません

qiita.com

Settings.Global

adb shellから以下のコマンドを実行することで、実行周期を変更できます

// 実行周期を15分に変更
adb shell settings put global auto_revoke_parameters enabledForPreRApps=false,unusedThresholdMs=60000,checkFrequencyMs=900000

// 設定できているかを確認
adb shell settings get global auto_revoke_parameters

DeviceConfig

adb shellから以下のコマンドを実行することで、実行周期を変更できます

// 実行周期を15分に変更
adb shell device_config put permissions auto_revoke_check_frequency_millis 900000

// 設定できているかを確認
adb shell device_config get permissions auto_revoke_check_frequency_millis

Auto-reset permissionsをすぐに実行したい!!

定期実行にJobSchedulerを使ってるので、adb shellから以下のコマンドを実行することで、Auto-reset permissionsをすぐに実行させることができます

adb shell cmd jobscheduler run -u 0 -f com.google.android.permissioncontroller 2

Auto-reset permissionsの動作確認をしたい時に便利です

ACTIVITY_RECOGNITION Permissionは自動リセットの対象外

Protection levelがDangerousに分類されるPermissionでも、android.Manifest.permission.ACTIVITY_RECOGNITIONのPermissionだけは自動リセットの対象外みたいです

private val EXEMPT_PERMISSIONS = listOf(
        android.Manifest.permission.ACTIVITY_RECOGNITION)

アプリの未使用期間はどのように調べているのか

UsageStatsManagerを利用してゴニョゴニョ調べているようです 詳細はコードを読んでみてください

Auto-reset permissionsの動作ログを見る

以下のlogcatで見れます

adb  logcat -v time -s AutoRevokePermissions

dumpsysから過去書き込んだログは見れます

adb shell dumpsys permissionmgr

Auto-reset permissionsの内部ステータスを見たい

dumpsysから色々見れます

adb shell dumpsys permissionmgr

自身のアプリのAuto-reset permissionsの設定値が知りたい

PackageManager#isAutoRevokeWhitelistedメソッドで知ることができます

developer.android.com

applicationContext.packageManager.isAutoRevokeWhitelisted

自身のアプリのAuto-reset permissions設定画面に遷移させたい

Intent.ACTION_AUTO_REVOKE_PERMISSIONSで遷移できます

developer.android.com

 val i = Intent(Intent.ACTION_AUTO_REVOKE_PERMISSIONS).apply {
                data = Uri.fromParts("package", packageName, null)
            }
startActivity(i)

おわりに

調べてみると公式ドキュメントには記載されていないことがいくつかわかったので面白かったです Permissionの実装を普通に行っていれば、Auto-reset permissionsは何も面倒なことではないのですが、詳細な動作が知りたくて調べてみました 久々にAndroidの一機能の内部実装を読んでみたのですが、Android Code Searchがとにかく便利だった!

今回Auto-reset permissionsを調べる上で、ちょっと書いたコードをGitHubで公開してます

github.com

Dart・Flutterを学ぶ日々 その13

MoorのMigrations

結構簡単だった

moor.simonbinder.eu

package_info

設定画面とかによくあるアプリのバージョンとか表示するためには、package_info経由で情報と取ってきて表示するのが良さそう

pub.dev

url_launcher

外部ブラウザでリンクを開くならこれ使う

pub.dev

FlutterFire

FirebaseのFlutter Pluginがまとまってるページ

firebase.flutter.dev

Dart・Flutterを学ぶ日々 その12

kIsWeb、kDebugMode

アプリケーションがWebで動いているかどうかの判定に使える定数

api.flutter.dev

kDebugMode

アプリケーションがデバッグモードで動いてるかどうかの判定に使える定数

api.flutter.dev

kReleaseModeもある

private function

_をつけるのね

Unlike Java, Dart doesn’t have the keywords public, protected, and private. If an identifier starts with an underscore (_), it’s private to its library.

dart.dev

shared_preferences

ローカルで値を保存・取得したい場合のplugin

pub.dev

SimpleDialog

ダイアログでメニュー選択式のやつ実装する時に使える

api.flutter.dev

Flutterの基本的なレイアウトの話

めっちゃ助かる

qiita.com

ListViewをColumnに格納する方法【Flutter】

同じく困ったので助かった

qiita.com

Text widget 使い方あれこれ

bukiyo-papa.com

任意のWidgetにクリックイベントをつける

qiita.com

flutter.dev

Dart・Flutterを学ぶ日々 その11

Flutter build release channels

flutter channel とかでbetaとかに変えられるのね

github.com

Changelog

更新されてそうな雰囲気なので読むと良さそう

github.com

Flutter Gallery

良さそう

github.com

Dart VM Service Protocol

JSON-RPC 2.0なのか

github.com

Null safety Flutter example

読んでおこう

github.com

DartPad

さくっとDartを動かしたいならこれが楽

dartpad.dev

dart.dev

Dart・Flutterを学ぶ日々 その10

Using the Logging view

デバッグの際に使えそう

flutter.dev

logging

Dartコマンドラインツールでのloggingにこれを使ってみる

github.com

Type test operators

nullにasをやるとnullが返るね。例外にはならないのか。

void main() {
  final a = null;
  print(a as String); // null
  (a as String).toString(); // これは例外になるから   (a as String ?? '').toString(); とかnullである可能性も意識したほうがいい
}

dart.dev

JSON and serialization

DartJSON扱う上で読んでおくと良さそう

flutter.dev

Dartのコードを試験的に書いておいておく場所

github.com

Flutter アーキテクチャ ガイド

技術書典9で購入したので、読み始めた

techbookfest.org