Merge "Show "review notification permissions" notification" into tm-dev am: a436e42912

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17716555

Change-Id: I9fd2b3774d0a6b49277eb8bffc2b634212eb7942
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Yuri Lin
2022-05-04 17:38:19 +00:00
committed by Automerger Merge Worker
10 changed files with 798 additions and 8 deletions

View File

@@ -7056,6 +7056,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>

View File

@@ -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();
}

View File

@@ -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.

View File

@@ -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) {

View File

@@ -0,0 +1,79 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,122 @@
/*
* 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.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.provider.Settings;
import android.util.Log;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
/**
* Broadcast receiver for intents that come from the "review notification permissions" notification,
* shown to users who upgrade to T from an earlier OS to inform them of notification setup changes
* and invite them to review their notification permissions.
*/
public class ReviewNotificationPermissionsReceiver extends BroadcastReceiver {
public static final String TAG = "ReviewNotifPermissions";
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
// 7 days in millis, as the amount of time to wait before re-sending the notification
private static final long JOB_RESCHEDULE_TIME = 1000 /* millis */ * 60 /* seconds */
* 60 /* minutes */ * 24 /* hours */ * 7 /* days */;
static IntentFilter getFilter() {
IntentFilter filter = new IntentFilter();
filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND);
filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS);
filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
return filter;
}
// Cancels the "review notification permissions" notification.
@VisibleForTesting
protected void cancelNotification(Context context) {
NotificationManager nm = context.getSystemService(NotificationManager.class);
if (nm != null) {
nm.cancel(NotificationManagerService.TAG,
SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS);
} else {
Slog.w(TAG, "could not cancel notification: NotificationManager not found");
}
}
@VisibleForTesting
protected void rescheduleNotification(Context context) {
ReviewNotificationPermissionsJobService.scheduleJob(context, JOB_RESCHEDULE_TIME);
// log if needed
if (DEBUG) {
Slog.d(TAG, "Scheduled review permissions notification for on or after: "
+ LocalDateTime.now(ZoneId.systemDefault())
.plus(JOB_RESCHEDULE_TIME, ChronoUnit.MILLIS));
}
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND)) {
// Reschedule the notification for 7 days in the future
rescheduleNotification(context);
// note that the user has interacted; no longer needed to show the initial
// notification
Settings.Global.putInt(context.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
cancelNotification(context);
} else if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS)) {
// user dismissed; write to settings so we don't show ever again
Settings.Global.putInt(context.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED);
cancelNotification(context);
} else if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED)) {
// we may get here from the user swiping away the notification,
// or from the notification being canceled in any other way.
// only in the case that the user hasn't interacted with it in
// any other way yet, reschedule
int notifState = Settings.Global.getInt(context.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
/* default */ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN);
if (notifState == NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW) {
// user hasn't interacted in the past, so reschedule once and then note that the
// user *has* interacted now so we don't re-reschedule if they swipe again
rescheduleNotification(context);
Settings.Global.putInt(context.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
} else if (notifState == NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN) {
// swiping away on a rescheduled notification; mark as interacted and
// don't reschedule again.
Settings.Global.putInt(context.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
}
}
}
}

View File

@@ -198,6 +198,7 @@ import com.android.internal.app.IAppOpsService;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.logging.InstanceIdSequenceFake;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.server.DeviceIdleInternal;
import com.android.server.LocalServices;
@@ -303,6 +304,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
ActivityManagerInternal mAmi;
@Mock
private Looper mMainLooper;
@Mock
private NotificationManager mMockNm;
@Mock
IIntentSender pi1;
@@ -405,6 +408,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
LocalServices.removeServiceForTest(PermissionPolicyInternal.class);
LocalServices.addService(PermissionPolicyInternal.class, mPermissionPolicyInternal);
mContext.addMockSystemService(Context.ALARM_SERVICE, mAlarmManager);
mContext.addMockSystemService(NotificationManager.class, mMockNm);
doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any());
@@ -9294,4 +9298,77 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
// the notifyPostedLocked function is called twice.
verify(mListeners, times(2)).notifyPostedLocked(any(), any());
}
@Test
public void testMaybeShowReviewPermissionsNotification_unknown() {
// Set up various possible states of the settings int and confirm whether or not the
// notification is shown as expected
// Initial state: default/unknown setting, make sure nothing happens
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN);
mService.maybeShowInitialReviewPermissionsNotification();
verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class));
}
@Test
public void testMaybeShowReviewPermissionsNotification_shouldShow() {
// If state is SHOULD_SHOW, it ... should show
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
mService.maybeShowInitialReviewPermissionsNotification();
verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG),
eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS),
any(Notification.class));
}
@Test
public void testMaybeShowReviewPermissionsNotification_alreadyShown() {
// If state is either USER_INTERACTED or DISMISSED, we should not show this on boot
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
mService.maybeShowInitialReviewPermissionsNotification();
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED);
mService.maybeShowInitialReviewPermissionsNotification();
verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class));
}
@Test
public void testMaybeShowReviewPermissionsNotification_reshown() {
// If we have re-shown the notification and the user did not subsequently interacted with
// it, then make sure we show when trying on boot
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
mService.maybeShowInitialReviewPermissionsNotification();
verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG),
eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS),
any(Notification.class));
}
@Test
public void testRescheduledReviewPermissionsNotification() {
// when rescheduled, the notification goes through the NotificationManagerInternal service
// this call doesn't need to know anything about previously scheduled state -- if called,
// it should send the notification & write the appropriate int to Settings
mInternalService.sendReviewPermissionsNotification();
// Notification should be sent
verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG),
eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS),
any(Notification.class));
// write STATE_RESHOWN to settings
assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN,
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
}

View File

@@ -289,6 +289,11 @@ public class PreferencesHelperTest extends UiServiceTestCase {
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
.build();
// make sure that the settings for review notification permissions are unset to begin with
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN);
}
private ByteArrayOutputStream writeXmlAndPurge(
@@ -656,6 +661,13 @@ public class PreferencesHelperTest extends UiServiceTestCase {
verify(mPermissionHelper).setNotificationPermission(nMr1Expected);
verify(mPermissionHelper).setNotificationPermission(oExpected);
verify(mPermissionHelper).setNotificationPermission(pExpected);
// verify that we also write a state for review_permissions_notification to eventually
// show a notification
assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW,
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
@Test
@@ -738,7 +750,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
}
@Test
public void testReadXml_newXml_noMigration() throws Exception {
public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception {
when(mPermissionHelper.isMigrationEnabled()).thenReturn(true);
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory);
@@ -786,6 +798,70 @@ public class PreferencesHelperTest extends UiServiceTestCase {
compareChannels(idp, mHelper.getNotificationChannel(PKG_P, UID_P, idp.getId(), false));
verify(mPermissionHelper, never()).setNotificationPermission(any());
// verify that we do, however, write a state for review_permissions_notification to
// eventually show a notification, since this XML version is older than the notification
assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW,
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
@Test
public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception {
when(mPermissionHelper.isMigrationEnabled()).thenReturn(true);
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory);
String xml = "<ranking version=\"4\">\n"
+ "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
+ "<channel id=\"idn\" name=\"name\" importance=\"2\"/>\n"
+ "<channel id=\"miscellaneous\" name=\"Uncategorized\" />\n"
+ "</package>\n"
+ "<package name=\"" + PKG_O + "\" >\n"
+ "<channel id=\"ido\" name=\"name2\" importance=\"2\" show_badge=\"true\"/>\n"
+ "</package>\n"
+ "<package name=\"" + PKG_P + "\" >\n"
+ "<channel id=\"idp\" name=\"name3\" importance=\"4\" locked=\"2\" />\n"
+ "</package>\n"
+ "</ranking>\n";
NotificationChannel idn = new NotificationChannel("idn", "name", IMPORTANCE_LOW);
idn.setSound(null, new AudioAttributes.Builder()
.setUsage(USAGE_NOTIFICATION)
.setContentType(CONTENT_TYPE_SONIFICATION)
.setFlags(0)
.build());
idn.setShowBadge(false);
NotificationChannel ido = new NotificationChannel("ido", "name2", IMPORTANCE_LOW);
ido.setShowBadge(true);
ido.setSound(null, new AudioAttributes.Builder()
.setUsage(USAGE_NOTIFICATION)
.setContentType(CONTENT_TYPE_SONIFICATION)
.setFlags(0)
.build());
NotificationChannel idp = new NotificationChannel("idp", "name3", IMPORTANCE_HIGH);
idp.lockFields(2);
idp.setSound(null, new AudioAttributes.Builder()
.setUsage(USAGE_NOTIFICATION)
.setContentType(CONTENT_TYPE_SONIFICATION)
.setFlags(0)
.build());
loadByteArrayXml(xml.getBytes(), true, USER_SYSTEM);
assertTrue(mHelper.canShowBadge(PKG_N_MR1, UID_N_MR1));
assertEquals(idn, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, idn.getId(), false));
compareChannels(ido, mHelper.getNotificationChannel(PKG_O, UID_O, ido.getId(), false));
compareChannels(idp, mHelper.getNotificationChannel(PKG_P, UID_P, idp.getId(), false));
verify(mPermissionHelper, never()).setNotificationPermission(any());
// this XML is new enough, we should not be attempting to show a notification or anything
assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN,
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
@Test
@@ -903,7 +979,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
ByteArrayOutputStream baos = writeXmlAndPurge(
PKG_N_MR1, UID_N_MR1, false, USER_SYSTEM);
String expected = "<ranking version=\"3\">\n"
String expected = "<ranking version=\"4\">\n"
+ "<package name=\"com.example.o\" show_badge=\"true\" "
+ "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" "
+ "sent_valid_msg=\"false\" user_demote_msg_app=\"false\" uid=\"1111\">\n"
@@ -984,7 +1060,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
ByteArrayOutputStream baos = writeXmlAndPurge(
PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM);
String expected = "<ranking version=\"3\">\n"
String expected = "<ranking version=\"4\">\n"
// Importance 0 because off in permissionhelper
+ "<package name=\"com.example.o\" importance=\"0\" show_badge=\"true\" "
+ "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" "
@@ -1067,7 +1143,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
ByteArrayOutputStream baos = writeXmlAndPurge(
PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM);
String expected = "<ranking version=\"3\">\n"
String expected = "<ranking version=\"4\">\n"
// Importance 0 because off in permissionhelper
+ "<package name=\"com.example.o\" importance=\"0\" show_badge=\"true\" "
+ "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" "
@@ -1121,7 +1197,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
ByteArrayOutputStream baos = writeXmlAndPurge(
PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM);
String expected = "<ranking version=\"3\">\n"
String expected = "<ranking version=\"4\">\n"
// Packages that exist solely in permissionhelper
+ "<package name=\"" + PKG_P + "\" importance=\"3\" />\n"
+ "<package name=\"" + PKG_O + "\" importance=\"0\" />\n"

View File

@@ -0,0 +1,104 @@
/*
* 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 static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.testing.AndroidTestingRunner;
import androidx.test.rule.ServiceTestRule;
import com.android.server.LocalServices;
import com.android.server.UiServiceTestCase;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
@RunWith(AndroidTestingRunner.class)
public class ReviewNotificationPermissionsJobServiceTest extends UiServiceTestCase {
private ReviewNotificationPermissionsJobService mJobService;
private JobParameters mJobParams = new JobParameters(null,
ReviewNotificationPermissionsJobService.JOB_ID, null, null, null,
0, false, false, null, null, null);
@Captor
ArgumentCaptor<JobInfo> mJobInfoCaptor;
@Mock
private JobScheduler mMockJobScheduler;
@Mock
private NotificationManagerInternal mMockNotificationManagerInternal;
@Rule
public final ServiceTestRule mServiceRule = new ServiceTestRule();
@Before
public void setUp() throws Exception {
mJobService = new ReviewNotificationPermissionsJobService();
mContext.addMockSystemService(JobScheduler.class, mMockJobScheduler);
// add NotificationManagerInternal to LocalServices
LocalServices.removeServiceForTest(NotificationManagerInternal.class);
LocalServices.addService(NotificationManagerInternal.class,
mMockNotificationManagerInternal);
}
@Test
public void testScheduleJob() {
// if asked, the job doesn't currently exist yet
when(mMockJobScheduler.getPendingJob(anyInt())).thenReturn(null);
final int rescheduleTimeMillis = 350; // arbitrary number
// attempt to schedule the job
ReviewNotificationPermissionsJobService.scheduleJob(mContext, rescheduleTimeMillis);
verify(mMockJobScheduler, times(1)).schedule(mJobInfoCaptor.capture());
// verify various properties of the job that is passed in to the job scheduler
JobInfo jobInfo = mJobInfoCaptor.getValue();
assertEquals(ReviewNotificationPermissionsJobService.JOB_ID, jobInfo.getId());
assertEquals(rescheduleTimeMillis, jobInfo.getMinLatencyMillis());
assertTrue(jobInfo.isPersisted()); // should continue after reboot
assertFalse(jobInfo.isPeriodic()); // one time
}
@Test
public void testOnStartJob() {
// the job need not be persisted after it does its work, so it'll return
// false
assertFalse(mJobService.onStartJob(mJobParams));
// verify that starting the job causes the notification to be sent
verify(mMockNotificationManagerInternal).sendReviewPermissionsNotification();
}
}

View File

@@ -0,0 +1,206 @@
/*
* 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 static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import android.content.Context;
import android.content.Intent;
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.server.UiServiceTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class ReviewNotificationPermissionsReceiverTest extends UiServiceTestCase {
// Simple mock class that just overrides the reschedule and cancel behavior so that it's easy
// to tell whether the receiver has sent requests to either reschedule or cancel the
// notification (or both).
private class MockReviewNotificationPermissionsReceiver
extends ReviewNotificationPermissionsReceiver {
boolean mCanceled = false;
boolean mRescheduled = false;
@Override
protected void cancelNotification(Context context) {
mCanceled = true;
}
@Override
protected void rescheduleNotification(Context context) {
mRescheduled = true;
}
}
private MockReviewNotificationPermissionsReceiver mReceiver;
private Intent mIntent;
@Before
public void setUp() {
mReceiver = new MockReviewNotificationPermissionsReceiver();
mIntent = new Intent(); // actions will be set in test cases
}
@Test
public void testReceive_remindMeLater_firstTime() {
// Test what happens when we receive a "remind me later" intent coming from
// a previously-not-interacted notification
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
// set up Intent action
mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND);
// Upon receipt of the intent, the following things should happen:
// - notification rescheduled
// - notification explicitly canceled
// - settings state updated to indicate user has interacted
mReceiver.onReceive(mContext, mIntent);
assertTrue(mReceiver.mRescheduled);
assertTrue(mReceiver.mCanceled);
assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
@Test
public void testReceive_remindMeLater_laterTimes() {
// Test what happens when we receive a "remind me later" intent coming from
// a previously-interacted notification that has been rescheduled
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
// set up Intent action
mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND);
// Upon receipt of the intent, the following things should still happen
// regardless of the fact that the user has interacted before:
// - notification rescheduled
// - notification explicitly canceled
// - settings state still indicate user has interacted
mReceiver.onReceive(mContext, mIntent);
assertTrue(mReceiver.mRescheduled);
assertTrue(mReceiver.mCanceled);
assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
@Test
public void testReceive_dismiss() {
// Test that dismissing the notification does *not* reschedule the notification,
// does cancel it, and writes that it has been dismissed to settings
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
// set up Intent action
mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS);
// send intent, watch what happens
mReceiver.onReceive(mContext, mIntent);
assertFalse(mReceiver.mRescheduled);
assertTrue(mReceiver.mCanceled);
assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED,
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
@Test
public void testReceive_notificationCanceled_firstSwipe() {
// Test the basic swipe away case: the first time the user swipes the notification
// away, it will not have been interacted with yet, so make sure it's rescheduled
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
// set up Intent action, would be called from notification's delete intent
mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
// send intent, make sure it gets:
// - rescheduled
// - not explicitly canceled, the notification was already canceled
// - noted that it's been interacted with
mReceiver.onReceive(mContext, mIntent);
assertTrue(mReceiver.mRescheduled);
assertFalse(mReceiver.mCanceled);
assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
@Test
public void testReceive_notificationCanceled_secondSwipe() {
// Test the swipe away case for a rescheduled notification: in this case
// it should not be rescheduled anymore
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
// set up Intent action, would be called from notification's delete intent
mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
// send intent, make sure it gets:
// - not rescheduled on the second+ swipe
// - not explicitly canceled, the notification was already canceled
// - mark as user interacted
mReceiver.onReceive(mContext, mIntent);
assertFalse(mReceiver.mRescheduled);
assertFalse(mReceiver.mCanceled);
assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
@Test
public void testReceive_notificationCanceled_fromDismiss() {
// Test that if the notification delete intent is called due to us canceling
// the notification from the receiver, we don't do anything extra
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED);
// set up Intent action, would be called from notification's delete intent
mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
// nothing should happen, nothing at all
mReceiver.onReceive(mContext, mIntent);
assertFalse(mReceiver.mRescheduled);
assertFalse(mReceiver.mCanceled);
assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED,
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
}