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对用户拒绝状态检查的限制。