Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5dfa2dbc authored by Yuri Lin's avatar Yuri Lin
Browse files

Show "review notification permissions" notification

This notification is shown for users who have XML version <4 (newest for now), which includes both users newly upgrading from <T and anyone who has already upgraded to T but not yet seen the notification.

This state is tracked using a Settings int that allows for coordination between PreferencesHelper (which reads XML), NMS (shows notification), and the new ReviewNotificationPermissionsReceiver (receives intents from the notification and behaves accordingly).

Change also includes a new JobService in order to schedule the job to re-show the notification 7 days in the future if the user requests it.

Fixes: 225373531
Test: manual; NotificationManagerServiceTest; PreferencesHelperTest; ReviewNotificationPermissionsReceiverTest; ReviewNotificationPermissionsJobServiceTest

Change-Id: I4039728646f706bdc1c01f2c0165721a5b10c9f5
parent 8b7783d4
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -7057,6 +7057,10 @@
                 android:permission="android.permission.BIND_JOB_SERVICE">
        </service>

        <service android:name="com.android.server.notification.ReviewNotificationPermissionsJobService"
                 android:permission="android.permission.BIND_JOB_SERVICE">
        </service>

        <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader"
            android:exported="false">
            <intent-filter>
+3 −0
Original line number Diff line number Diff line
@@ -42,4 +42,7 @@ public interface NotificationManagerInternal {

    /** Does the specified package/uid have permission to post notifications? */
    boolean areNotificationsEnabledForPackage(String pkg, int uid);

    /** Send a notification to the user prompting them to review their notification permissions. */
    void sendReviewPermissionsNotification();
}
+107 −0
Original line number Diff line number Diff line
@@ -274,6 +274,7 @@ import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
@@ -442,6 +443,18 @@ public class NotificationManagerService extends SystemService {
    private static final int NOTIFICATION_INSTANCE_ID_MAX = (1 << 13);
    // States for the review permissions notification
    static final int REVIEW_NOTIF_STATE_UNKNOWN = -1;
    static final int REVIEW_NOTIF_STATE_SHOULD_SHOW = 0;
    static final int REVIEW_NOTIF_STATE_USER_INTERACTED = 1;
    static final int REVIEW_NOTIF_STATE_DISMISSED = 2;
    static final int REVIEW_NOTIF_STATE_RESHOWN = 3;
    // Action strings for review permissions notification
    static final String REVIEW_NOTIF_ACTION_REMIND = "REVIEW_NOTIF_ACTION_REMIND";
    static final String REVIEW_NOTIF_ACTION_DISMISS = "REVIEW_NOTIF_ACTION_DISMISS";
    static final String REVIEW_NOTIF_ACTION_CANCELED = "REVIEW_NOTIF_ACTION_CANCELED";
    /**
     * Apps that post custom toasts in the background will have those blocked. Apps can
     * still post toasts created with
@@ -652,6 +665,9 @@ public class NotificationManagerService extends SystemService {
    private InstanceIdSequence mNotificationInstanceIdSequence;
    private Set<String> mMsgPkgsAllowedAsConvos = new HashSet();
    // Broadcast intent receiver for notification permissions review-related intents
    private ReviewNotificationPermissionsReceiver mReviewNotificationPermissionsReceiver;
    static class Archive {
        final SparseArray<Boolean> mEnabled;
        final int mBufferSize;
@@ -2416,6 +2432,11 @@ public class NotificationManagerService extends SystemService {
        IntentFilter localeChangedFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
        getContext().registerReceiver(mLocaleChangeReceiver, localeChangedFilter);
        mReviewNotificationPermissionsReceiver = new ReviewNotificationPermissionsReceiver();
        getContext().registerReceiver(mReviewNotificationPermissionsReceiver,
                ReviewNotificationPermissionsReceiver.getFilter(),
                Context.RECEIVER_NOT_EXPORTED);
    }
    /**
@@ -2709,6 +2730,7 @@ public class NotificationManagerService extends SystemService {
            mHistoryManager.onBootPhaseAppsCanStart();
            registerDeviceConfigChange();
            migrateDefaultNAS();
            maybeShowInitialReviewPermissionsNotification();
        } else if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
            mSnoozeHelper.scheduleRepostsForPersistedNotifications(System.currentTimeMillis());
        }
@@ -6336,6 +6358,21 @@ public class NotificationManagerService extends SystemService {
        public boolean areNotificationsEnabledForPackage(String pkg, int uid) {
            return areNotificationsEnabledForPackageInt(pkg, uid);
        }
        @Override
        public void sendReviewPermissionsNotification() {
            // This method is meant to be called from the JobService upon running the job for this
            // notification having been rescheduled; so without checking any other state, it will
            // send the notification.
            checkCallerIsSystem();
            NotificationManager nm = getContext().getSystemService(NotificationManager.class);
            nm.notify(TAG,
                    SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS,
                    createReviewPermissionsNotification());
            Settings.Global.putInt(getContext().getContentResolver(),
                    Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
                    NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
        }
    };
    int getNumNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted) {
@@ -11608,6 +11645,76 @@ public class NotificationManagerService extends SystemService {
        out.endTag(null, LOCKSCREEN_ALLOW_SECURE_NOTIFICATIONS_TAG);
    }
    // Creates a notification that informs the user about changes due to the migration to
    // use permissions for notifications.
    protected Notification createReviewPermissionsNotification() {
        int title = R.string.review_notification_settings_title;
        int content = R.string.review_notification_settings_text;
        // Tapping on the notification leads to the settings screen for managing app notifications,
        // using the intent reserved for system services to indicate it comes from this notification
        Intent tapIntent = new Intent(Settings.ACTION_ALL_APPS_NOTIFICATION_SETTINGS_FOR_REVIEW);
        Intent remindIntent = new Intent(REVIEW_NOTIF_ACTION_REMIND);
        Intent dismissIntent = new Intent(REVIEW_NOTIF_ACTION_DISMISS);
        Intent swipeIntent = new Intent(REVIEW_NOTIF_ACTION_CANCELED);
        // Both "remind me" and "dismiss" actions will be actions received by the BroadcastReceiver
        final Notification.Action remindMe = new Notification.Action.Builder(null,
                getContext().getResources().getString(
                        R.string.review_notification_settings_remind_me_action),
                PendingIntent.getBroadcast(
                        getContext(), 0, remindIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
                .build();
        final Notification.Action dismiss = new Notification.Action.Builder(null,
                getContext().getResources().getString(
                        R.string.review_notification_settings_dismiss),
                PendingIntent.getBroadcast(
                        getContext(), 0, dismissIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
                .build();
        return new Notification.Builder(getContext(), SystemNotificationChannels.SYSTEM_CHANGES)
                .setSmallIcon(R.drawable.stat_sys_adb)
                .setContentTitle(getContext().getResources().getString(title))
                .setContentText(getContext().getResources().getString(content))
                .setContentIntent(PendingIntent.getActivity(getContext(), 0, tapIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
                .setStyle(new Notification.BigTextStyle())
                .setFlag(Notification.FLAG_NO_CLEAR, true)
                .setAutoCancel(true)
                .addAction(remindMe)
                .addAction(dismiss)
                .setDeleteIntent(PendingIntent.getBroadcast(getContext(), 0, swipeIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
                .build();
    }
    protected void maybeShowInitialReviewPermissionsNotification() {
        int currentState = Settings.Global.getInt(getContext().getContentResolver(),
                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
                REVIEW_NOTIF_STATE_UNKNOWN);
        // now check the last known state of the notification -- this determination of whether the
        // user is in the correct target audience occurs elsewhere, and will have written the
        // REVIEW_NOTIF_STATE_SHOULD_SHOW to indicate it should be shown in the future.
        //
        // alternatively, if the user has rescheduled the notification (so it has been shown
        // again) but not yet interacted with the new notification, then show it again on boot,
        // as this state indicates that the user had the notification open before rebooting.
        //
        // sending the notification here does not record a new state for the notification;
        // that will be written by parts of the system further down the line if at any point
        // the user interacts with the notification.
        if (currentState == REVIEW_NOTIF_STATE_SHOULD_SHOW
                || currentState == REVIEW_NOTIF_STATE_RESHOWN) {
            NotificationManager nm = getContext().getSystemService(NotificationManager.class);
            nm.notify(TAG,
                    SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS,
                    createReviewPermissionsNotification());
        }
    }
    /**
     * Shows a warning on logcat. Shows the toast only once per package. This is to avoid being too
     * aggressive and annoying the user.
+15 −3
Original line number Diff line number Diff line
@@ -96,6 +96,10 @@ public class PreferencesHelper implements RankingConfig {
    private final int XML_VERSION;
    /** What version to check to do the upgrade for bubbles. */
    private static final int XML_VERSION_BUBBLES_UPGRADE = 1;
    /** The first xml version with notification permissions enabled. */
    private static final int XML_VERSION_NOTIF_PERMISSION = 3;
    /** The first xml version that notifies users to review their notification permissions */
    private static final int XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION = 4;
    @VisibleForTesting
    static final int UNKNOWN_UID = UserHandle.USER_NULL;
    private static final String NON_BLOCKABLE_CHANNEL_DELIM = ":";
@@ -206,7 +210,7 @@ public class PreferencesHelper implements RankingConfig {
        mStatsEventBuilderFactory = statsEventBuilderFactory;

        if (mPermissionHelper.isMigrationEnabled()) {
            XML_VERSION = 3;
            XML_VERSION = 4;
        } else {
            XML_VERSION = 2;
        }
@@ -226,8 +230,16 @@ public class PreferencesHelper implements RankingConfig {

        final int xmlVersion = parser.getAttributeInt(null, ATT_VERSION, -1);
        boolean upgradeForBubbles = xmlVersion == XML_VERSION_BUBBLES_UPGRADE;
        boolean migrateToPermission =
                (xmlVersion < XML_VERSION) && mPermissionHelper.isMigrationEnabled();
        boolean migrateToPermission = (xmlVersion < XML_VERSION_NOTIF_PERMISSION)
                && mPermissionHelper.isMigrationEnabled();
        if (xmlVersion < XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION) {
            // make a note that we should show the notification at some point.
            // it shouldn't be possible for the user to already have seen it, as the XML version
            // would be newer then.
            Settings.Global.putInt(mContext.getContentResolver(),
                    Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
                    NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
        }
        ArrayList<PermissionHelper.PackagePermission> pkgPerms = new ArrayList<>();
        synchronized (mPackagePreferences) {
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+79 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.notification;

import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;

/**
 * JobService implementation for scheduling the notification informing users about
 * notification permissions updates and taking them to review their existing permissions.
 * @hide
 */
public class ReviewNotificationPermissionsJobService extends JobService {
    public static final String TAG = "ReviewNotificationPermissionsJobService";

    @VisibleForTesting
    protected static final int JOB_ID = 225373531;

    /**
     *  Schedule a new job that will show a notification the specified amount of time in the future.
     */
    public static void scheduleJob(Context context, long rescheduleTimeMillis) {
        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
        // if the job already exists for some reason, cancel & reschedule
        if (jobScheduler.getPendingJob(JOB_ID) != null) {
            jobScheduler.cancel(JOB_ID);
        }
        ComponentName component = new ComponentName(
                context, ReviewNotificationPermissionsJobService.class);
        JobInfo newJob = new JobInfo.Builder(JOB_ID, component)
                .setPersisted(true) // make sure it'll still get rescheduled after reboot
                .setMinimumLatency(rescheduleTimeMillis)  // run after specified amount of time
                .build();
        jobScheduler.schedule(newJob);
    }

    @Override
    public boolean onStartJob(JobParameters params) {
        // While jobs typically should be run on different threads, this
        // job only posts a notification, which is not a long-running operation
        // as notification posting is asynchronous.
        NotificationManagerInternal nmi =
                LocalServices.getService(NotificationManagerInternal.class);
        nmi.sendReviewPermissionsNotification();

        // once the notification is posted, the job is done, so no need to
        // keep it alive afterwards
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        // If we're interrupted for some reason, try again (though this may not
        // ever happen due to onStartJob not leaving a job running after being
        // called)
        return true;
    }
}
Loading