Androidアプリでパーミッションを取得されているかどうかの確認を行う際、PermissionChecker.checkSelfPermission を使用していたのですが、「PermissionCheckerはAppOpsのチェックも行うので、通常はContextCompatを使うべき」と言われました(とあるレビューAIに)
ええ、違いがあるって意識してなかったヨ
実装を比較
まずは実装を比較してみます。
public static int checkSelfPermission(@NonNull Context context, @NonNull String permission) {
ObjectsCompat.requireNonNull(permission, "permission must be non-null");
if (Build.VERSION.SDK_INT < 33
&& TextUtils.equals(android.Manifest.permission.POST_NOTIFICATIONS, permission)) {
return NotificationManagerCompat.from(context).areNotificationsEnabled()
? PackageManager.PERMISSION_GRANTED
: PackageManager.PERMISSION_DENIED;
}
return context.checkPermission(permission, Process.myPid(), Process.myUid());
}
ContextCompatについては、API 33で追加されたPOST_NOTIFICATIONSについて互換性のための実装が入っているのみでContextのcheckPermissionの結果を返しているだけです。
@PermissionResult
public static int checkPermission(@NonNull Context context, @NonNull String permission,
int pid, int uid, @Nullable String packageName) {
if (context.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_DENIED) {
return PERMISSION_DENIED;
}
String op = AppOpsManagerCompat.permissionToOp(permission);
if (op == null) {
return PERMISSION_GRANTED;
}
if (packageName == null) {
String[] packageNames = context.getPackageManager().getPackagesForUid(uid);
if (packageNames == null || packageNames.length <= 0) {
return PERMISSION_DENIED;
}
packageName = packageNames[0];
}
int proxyUid = android.os.Process.myUid();
String proxyPackageName = context.getPackageName();
boolean isCheckSelfPermission =
proxyUid == uid && ObjectsCompat.equals(proxyPackageName, packageName);
int checkOpResult;
if (isCheckSelfPermission) {
checkOpResult = AppOpsManagerCompat.checkOrNoteProxyOp(context, uid, op, packageName);
} else {
checkOpResult = AppOpsManagerCompat.noteProxyOpNoThrow(context, op, packageName);
}
return checkOpResult == AppOpsManagerCompat.MODE_ALLOWED ? PERMISSION_GRANTED :
PERMISSION_DENIED_APP_OP;
}
こちらはContextのcheckPermissionでチェックして、PERMISSION_DENIEDであれば終了。
PERMISSION_GRANTEDの場合にAppOpsManagerCompatでチェックを行っています。
このAppOpsManagerはPermissionと比べると使う機会はかなり少ないのですが、簡単に言うと、システム設定としてその権限が許可されているかをチェックしています。
実動作の違い
というわけで、以下のようにLogcatに出力して動作を比較してみます
private fun checkPermission(permission: String) {
Log.e("XXXX", "ContextCompat ${contextCompat(permission)} $permission")
Log.e("XXXX", "PermissionChecker ${permissionChecker(permission)} $permission")
}
private fun contextCompat(permission: String): String =
when (ContextCompat.checkSelfPermission(this, permission)) {
PackageManager.PERMISSION_GRANTED -> "GRANTED"
PackageManager.PERMISSION_DENIED -> "DENIED"
else -> "UNKNOWN"
}
private fun permissionChecker(permission: String): String =
when (PermissionChecker.checkSelfPermission(this, permission)) {
PermissionChecker.PERMISSION_GRANTED -> "GRANTED"
PermissionChecker.PERMISSION_DENIED -> "DENIED"
PermissionChecker.PERMISSION_DENIED_APP_OP -> "DENIED_APP_OP"
else -> "UNKNOWN"
}
CAMERA / RECORD_AUDIO / ACCESS_FINE_LOCATION について調べて見ます。
checkPermission(Manifest.permission.CAMERA)
checkPermission(Manifest.permission.RECORD_AUDIO)
checkPermission(Manifest.permission.ACCESS_FINE_LOCATION)
インストール直後
ContextCompat DENIED android.permission.CAMERA
PermissionChecker DENIED android.permission.CAMERA
ContextCompat DENIED android.permission.RECORD_AUDIO
PermissionChecker DENIED android.permission.RECORD_AUDIO
ContextCompat DENIED android.permission.ACCESS_FINE_LOCATION
PermissionChecker DENIED android.permission.ACCESS_FINE_LOCATION
当然すべてDENIEDになります
アプリの権限を許可
一通り、権限を許可してみます。
ContextCompat GRANTED android.permission.CAMERA
PermissionChecker GRANTED android.permission.CAMERA
ContextCompat GRANTED android.permission.RECORD_AUDIO
PermissionChecker GRANTED android.permission.RECORD_AUDIO
ContextCompat GRANTED android.permission.ACCESS_FINE_LOCATION
PermissionChecker GRANTED android.permission.ACCESS_FINE_LOCATION
当然すべてGRANTEDです。
システム設定で機能を無効化
続いて、システム設定で位置情報、マイク、カメラを無効化してみます。
ContextCompat GRANTED android.permission.CAMERA
PermissionChecker DENIED_APP_OP android.permission.CAMERA
ContextCompat GRANTED android.permission.RECORD_AUDIO
PermissionChecker DENIED_APP_OP android.permission.RECORD_AUDIO
ContextCompat GRANTED android.permission.ACCESS_FINE_LOCATION
PermissionChecker DENIED_APP_OP android.permission.ACCESS_FINE_LOCATION
ContextCompatではGRANTEDですが、PermissionCheckerではDENIED_APP_OPとなりました。
この状態がAppOpsによって許可されていない状態ですね。
まとめ
PermissionCheckerでは、ランタイムパーミッションによる権限を獲得しているかだけでなく、システム設定で許可されているかも含めてチェックを行っています。DENIED_APP_OPとなった場合、requestPermissionsなどを使って、ランタイムパーミッションを要求してもすでに獲得済みなので解決できません。システム設定に遷移して許可してもらうなどの手続きが必要です。
そのため、単にランタイムパーミッションの権限有無を確認して、ランタイムパーミッションをリクエストする処理であれば、PermissionCheckerではなく、ContextCompatを使った方が良いです。
一方、AppOpsによって機能が使えない可能性のあるパーミッションについて、実際にその機能が使えるかどうか?の判定を行いたい場合や、DENIED_APP_OPとなった場合に、システム設定へ案内するなどを行う場合はPermissionCheckerを使う必要があります。
以上です。