Android 13 通知权限适配弹框原理分析

Android 13 通知权限适配弹框原理分析

背景

对于以低于 Android 13 的版本的 SDK 为目标平台的应用,在应用创建至少一个 NotificationChannel 后,拦截首次 activity 启动以显示权限提示,询问用户是否想要接收来自应用的通知。简单来说就是targetSDK在Android 13以前的应用,如果至少有一个NotificationChannel,则在首次Activity启动时会自动弹出通知权限授权。主要分析该机制的原理。来源:https://source.android.com/docs/core/display/notification-perm

分析

在com.android.server.policy.PermissionPolicyService$Internal中,会在ActivityManager准备好后立即注册一个ActivityStartInterceptor:

private void onActivityManagerReady() {

ActivityTaskManagerInternal atm =

LocalServices.getService(ActivityTaskManagerInternal.class);

atm.registerActivityStartInterceptor(

ActivityInterceptorCallback.PERMISSION_POLICY_ORDERED_ID,

mActivityInterceptorCallback);

}

实现以下逻辑:

private final ActivityInterceptorCallback mActivityInterceptorCallback =

new ActivityInterceptorCallback() {

@Nullable

@Override

public ActivityInterceptorCallback.ActivityInterceptResult

onInterceptActivityLaunch(@NonNull ActivityInterceptorInfo info) {

return null;

}

@Override

public void onActivityLaunched(TaskInfo taskInfo, ActivityInfo activityInfo,

ActivityInterceptorInfo info) {

if (!shouldShowNotificationDialogOrClearFlags(taskInfo,

activityInfo.packageName, info.getCallingPackage(),

info.getIntent(), info.getCheckedOptions(), activityInfo.name,

true)

|| isNoDisplayActivity(activityInfo)) {

return;

}

UserHandle user = UserHandle.of(taskInfo.userId);

if (!CompatChanges.isChangeEnabled(NOTIFICATION_PERM_CHANGE_ID,

activityInfo.packageName, user)) {

// Post the activity start checks to ensure the notification channel

// checks happen outside the WindowManager global lock.

mHandler.post(() -> showNotificationPromptIfNeeded(

activityInfo.packageName, taskInfo.userId, taskInfo.taskId,

info));

}

}

};

首先进入shouldShowNotificationDialogOrClearFlags判断,主要检查是否是首次打开的Activity,以及找到一个特定的Task用来弹出授权窗口:

/**

* Determine if a particular task is in the proper state to show a system-triggered

* permission prompt. A prompt can be shown if the task is just starting, or the task is

* currently focused, visible, and running, and,

* 1. The isEligibleForLegacyPermissionPrompt ActivityOption is set, or

* 2. The intent is a launcher intent (action is ACTION_MAIN, category is LAUNCHER), or

* 3. The activity belongs to the same package as the one which launched the task

* originally, and the task was started with a launcher intent, or

* 4. The activity is the first activity in a new task, and was started by the app the

* activity belongs to, and that app has another task that is currently focused, which was

* started with a launcher intent. This case seeks to identify cases where an app launches,

* then immediately trampolines to a new activity and task.

* @param taskInfo The task to be checked

* @param currPkg The package of the current top visible activity

* @param callingPkg The package that initiated this dialog action

* @param intent The intent of the current top visible activity

* @param options The ActivityOptions of the newly started activity, if this is called due

* to an activity start

* @param startedActivity The ActivityInfo of the newly started activity, if this is called

* due to an activity start

*/

private boolean shouldShowNotificationDialogOrClearFlags(TaskInfo taskInfo, String currPkg,

String callingPkg, Intent intent, ActivityOptions options,

String topActivityName, boolean startedActivity) {

if (intent == null || currPkg == null || taskInfo == null || topActivityName == null

|| (!(taskInfo.isFocused && taskInfo.isVisible && taskInfo.isRunning)

&& !startedActivity)) {

return false;

}

return isLauncherIntent(intent)

|| (options != null && options.isEligibleForLegacyPermissionPrompt())

|| isTaskStartedFromLauncher(currPkg, taskInfo)

|| (isTaskPotentialTrampoline(topActivityName, currPkg, callingPkg, taskInfo,

intent)

&& (!startedActivity || pkgHasRunningLauncherTask(currPkg, taskInfo)));

}

然后会post执行showNotificationPromptIfNeeded:

void showNotificationPromptIfNeeded(@NonNull String packageName, int userId,

int taskId, @Nullable ActivityInterceptorInfo info) {

UserHandle user = UserHandle.of(userId);

if (packageName == null || taskId == ActivityTaskManager.INVALID_TASK_ID

|| !shouldForceShowNotificationPermissionRequest(packageName, user)) {

return;

}

launchNotificationPermissionRequestDialog(packageName, user, taskId, info);

}

这里面会检查调用shouldForceShowNotificationPermissionRequest进行检查,这个检查比较关键:

private boolean shouldForceShowNotificationPermissionRequest(@NonNull String pkgName,

@NonNull UserHandle user) {

AndroidPackage pkg = mPackageManagerInternal.getPackage(pkgName);

if (pkg == null || pkg.getPackageName() == null

|| Objects.equals(pkgName, mPackageManager.getPermissionControllerPackageName())

|| pkg.getTargetSdkVersion() < Build.VERSION_CODES.M) {

if (pkg == null) {

Slog.w(LOG_TAG, "Cannot check for Notification prompt, no package for "

+ pkgName);

}

return false;

}

synchronized (mLock) {

if (!mBootCompleted) {

return false;

}

}

if (!pkg.getRequestedPermissions().contains(POST_NOTIFICATIONS)

|| CompatChanges.isChangeEnabled(NOTIFICATION_PERM_CHANGE_ID, pkgName, user)

|| mKeyguardManager.isKeyguardLocked()) {

return false;

}

int uid = user.getUid(pkg.getUid());

if (mNotificationManager == null) {

mNotificationManager = LocalServices.getService(NotificationManagerInternal.class);

}

boolean hasCreatedNotificationChannels = mNotificationManager

.getNumNotificationChannelsForPackage(pkgName, uid, true) > 0;

boolean granted = mPermissionManagerInternal.checkUidPermission(uid, POST_NOTIFICATIONS)

== PackageManager.PERMISSION_GRANTED;

int flags = mPackageManager.getPermissionFlags(POST_NOTIFICATIONS, pkgName, user);

boolean explicitlySet = (flags & PermissionManager.EXPLICIT_SET_FLAGS) != 0;

return !granted && hasCreatedNotificationChannels && !explicitlySet;

}

这里面比较关键的两个检查:

checkUidPermission:这个就不用说了,只有在未获得权限的时候才会弹窗

(flags & PermissionManager.EXPLICIT_SET_FLAGS) != 0:这个主要是看用户有没有曾经拒绝过权限,或者是否是被设备策略拒绝

EXPLICIT_SET_FLAGS的定义如下:

/**

* The set of flags that indicate that a permission state has been explicitly set

*

* @hide

*/

public static final int EXPLICIT_SET_FLAGS = FLAG_PERMISSION_USER_SET

| FLAG_PERMISSION_USER_FIXED | FLAG_PERMISSION_POLICY_FIXED

| FLAG_PERMISSION_SYSTEM_FIXED | FLAG_PERMISSION_GRANTED_BY_DEFAULT

| FLAG_PERMISSION_GRANTED_BY_ROLE;

可以看到不仅包含了FLAG_PERMISSION_USER_SET和FLAG_PERMISSION_USER_FIXED这些用户手动拒绝的情况,还包含了FLAG_PERMISSION_POLICY_FIXED等由于设备策略等原因拒绝的情况,以及一些其它情况。都判断完了之后就调用launchNotificationPermissionRequestDialog弹窗:

private void launchNotificationPermissionRequestDialog(String pkgName, UserHandle user,

int taskId, @Nullable ActivityInterceptorInfo info) {

Intent grantPermission = mPackageManager

.buildRequestPermissionsIntent(new String[] { POST_NOTIFICATIONS });

// Prevent the front-most activity entering pip due to overlay activity started on top.

grantPermission.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NO_USER_ACTION);

grantPermission.setAction(

ACTION_REQUEST_PERMISSIONS_FOR_OTHER);

grantPermission.putExtra(Intent.EXTRA_PACKAGE_NAME, pkgName);

final boolean remoteAnimation = info != null && info.getCheckedOptions() != null

&& info.getCheckedOptions().getAnimationType() == ANIM_REMOTE_ANIMATION

&& info.getClearOptionsAnimationRunnable() != null;

ActivityOptions options = remoteAnimation ? ActivityOptions.makeRemoteAnimation(

info.getCheckedOptions().getRemoteAnimationAdapter(),

info.getCheckedOptions().getRemoteTransition())

: new ActivityOptions(new Bundle());

options.setTaskOverlay(true, false);

options.setLaunchTaskId(taskId);

if (remoteAnimation) {

// Remote animation set on the intercepted activity will be handled by the grant

// permission activity, which is launched below. So we need to clear remote

// animation from the intercepted activity and its siblings to prevent duplication.

// This should trigger ActivityRecord#clearOptionsAnimationForSiblings for the

// intercepted activity.

info.getClearOptionsAnimationRunnable().run();

}

try {

mContext.startActivityAsUser(grantPermission, options.toBundle(), user);

} catch (Exception e) {

Log.e(LOG_TAG, "couldn't start grant permission dialog"

+ "for other package " + pkgName, e);

}

}

这个权限弹窗也是用的buildRequestPermissionsIntent返回的Intent,只不过action更换成了ACTION_REQUEST_PERMISSIONS_FOR_OTHER,并且添加了EXTRA_PACKAGE_NAME,然后ActivityOptions加了一些动画和taskId相关的,最后调用startActivityAsUser启动。

测试

实际上shouldForceShowNotificationPermissionRequest主要的控制点就是用户有没有拒绝过弹窗,但是实际上PermissionController中也会有类似的检查,如果用户拒绝过弹窗的话,也是不能弹出的,即使是system_server进行放行也不行,这个结论是使用hook进行的验证:

if (loadPackageParam.packageName.equals("android")) {

MethodHook hooker = new MethodHook.Builder(

"com.android.server.policy.PermissionPolicyService$Internal", loadPackageParam.classLoader)

.setMethodName("shouldForceShowNotificationPermissionRequest")

.addParameter(String.class)

.addParameter(UserHandle.class)

.setCallback(new XC_MethodHook() {

@Override

protected void afterHookedMethod(MethodHookParam param) {

Log.i(TAG, "Previous result: " + param.getResult());

param.setResult(true);

}

}).build();

MethodHook.normalInstall(hooker);

}

实际测试的时候,如果用户拒绝过权限弹窗,的确会出现Previous result: false。通过hook临时规避之后,已经看到system_server已经发出了Intent但是还是无法弹出权限授权窗口,推测是PermissionController中对FLAG_PERMISSION_USER_SET和FLAG_PERMISSION_USER_FIXED这类标记还有进一步的检查。顺带补充一下系统发送的Intent的内容:

Intent { act=android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER flg=0x10840000 pkg=com.google.android.permissioncontroller cmp=com.google.android.permissioncontroller/com.android.permissioncontroller.permission.ui.GrantPermissionsActivity (has extras) }

[extra] android.intent.extra.PACKAGE_NAME `java.lang.String`: com.example.test

[extra] android.content.pm.extra.REQUEST_PERMISSIONS_NAMES `[Ljava.lang.String;`: [android.permission.POST_NOTIFICATIONS]

[extra] android.content.pm.extra.REQUEST_PERMISSIONS_DEVICE_ID `java.lang.Integer`: 0

总结

本文记录了Android 13通知权限适配弹框生成的原理,本质上和普通权限弹窗一样,只不过是action有区别。然而即使是system_server发出的弹窗也会受到PermissionController对用户拒绝状态检查的限制。

相关推荐

无线路由器怎么分网
日博365客服电话

无线路由器怎么分网

07-10 👁️ 7146
在 Android 平台上测试应用
谁有365比分链接

在 Android 平台上测试应用

07-24 👁️ 9600
1970年世界杯决赛 巴西vs意大利 全场录像回放【掌触体育】
唱吧怎么了?App无法使用的原因和解决方法!