diff options
11 files changed, 522 insertions, 43 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index a0c5c2cf38dd..3fc9709ab3b1 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -303,6 +303,7 @@ package android { field public static final String RECEIVE_EMERGENCY_BROADCAST = "android.permission.RECEIVE_EMERGENCY_BROADCAST"; field @FlaggedApi("android.permission.flags.voice_activation_permission_apis") public static final String RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA = "android.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA"; field @FlaggedApi("android.permission.flags.voice_activation_permission_apis") public static final String RECEIVE_SANDBOX_TRIGGER_AUDIO = "android.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO"; + field @FlaggedApi("com.android.server.notification.flags.redact_otp_notifications_from_untrusted_listeners") public static final String RECEIVE_SENSITIVE_NOTIFICATIONS = "android.permission.RECEIVE_SENSITIVE_NOTIFICATIONS"; field public static final String RECEIVE_WIFI_CREDENTIAL_CHANGE = "android.permission.RECEIVE_WIFI_CREDENTIAL_CHANGE"; field public static final String RECORD_BACKGROUND_AUDIO = "android.permission.RECORD_BACKGROUND_AUDIO"; field public static final String RECOVERY = "android.permission.RECOVERY"; diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java index bb56939ac72f..264b53c6ee40 100644 --- a/core/java/android/service/notification/StatusBarNotification.java +++ b/core/java/android/service/notification/StatusBarNotification.java @@ -272,8 +272,10 @@ public class StatusBarNotification implements Parcelable { /** * @param notification Some kind of clone of this.notification. * @return A shallow copy of self, with notification in place of this.notification. + * + * @hide */ - StatusBarNotification cloneShallow(Notification notification) { + public StatusBarNotification cloneShallow(Notification notification) { StatusBarNotification result = new StatusBarNotification(this.pkg, this.opPkg, this.id, this.tag, this.uid, this.initialPid, notification, this.user, this.overrideGroupKey, this.postTime); diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig index 2a05c847f404..a2ade6a9473f 100644 --- a/core/java/android/service/notification/flags.aconfig +++ b/core/java/android/service/notification/flags.aconfig @@ -15,3 +15,9 @@ flag { bug: "299448097" } +flag { + name: "redact_sensitive_notifications_from_untrusted_listeners" + namespace: "systemui" + description: "This flag controls the redacting of sensitive notifications from untrusted NotificationListenerServices" + bug: "306271190" +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c6a241f2fa62..0264fdc198f3 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7855,6 +7855,17 @@ <permission android:name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" android:protectionLevel="signature|privileged" /> + <!-- @hide @SystemApi + @FlaggedApi("com.android.server.notification.flags.redact_otp_notifications_from_untrusted_listeners") + Allows apps with a NotificationListenerService to receive notifications with sensitive + information + <p>Apps with a NotificationListenerService without this permission will not be able + to view certain types of sensitive information contained in notifications + <p>Protection level: signature|role + --> + <permission android:name="android.permission.RECEIVE_SENSITIVE_NOTIFICATIONS" + android:protectionLevel="signature|role" /> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 4596ca74bf8f..d2fb9e12d069 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6363,4 +6363,10 @@ ul.</string> <!-- Communal profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] --> <string name="profile_label_communal">Communal</string> + <!-- Notification message used when a notification's normal message contains sensitive information. --> + <!-- TODO b/301960090: replace with redacted message string and action title, when/if UX provides one --> + <!-- DO NOT TRANSLATE --> + <string name="redacted_notification_message"></string> + <!-- Notification action title used instead of a notification's normal title sensitive [CHAR_LIMIT=NOTIF_BODY] --> + <string name="redacted_notification_action_title"></string> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index ef272ee8bef1..38943306b962 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5158,6 +5158,10 @@ <java-symbol type="string" name="keyboard_layout_notification_multiple_selected_title"/> <java-symbol type="string" name="keyboard_layout_notification_multiple_selected_message"/> + <!-- For redacted notifications --> + <java-symbol type="string" name="redacted_notification_message"/> + <java-symbol type="string" name="redacted_notification_action_title"/> + <java-symbol type="bool" name="config_batteryStatsResetOnUnplugHighBatteryLevel" /> <java-symbol type="bool" name="config_batteryStatsResetOnUnplugAfterSignificantCharge" /> <java-symbol type="integer" name="config_defaultPowerStatsThrottlePeriodCpu" /> diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index bacab0f8f1e8..6e65c16c41a1 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -882,6 +882,9 @@ <!-- Permissions required for CTS test - CtsAccessibilityServiceTestCases--> <uses-permission android:name="android.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING" /> + <!-- Permission required for Cts test - CtsNotificationTestCases --> + <uses-permission android:name="android.permission.RECEIVE_SENSITIVE_NOTIFICATIONS" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index f1029a335ab6..1a35f04b393f 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -238,6 +238,7 @@ <uses-permission android:name="android.permission.MANAGE_NOTIFICATIONS" /> <uses-permission android:name="android.permission.GET_RUNTIME_PERMISSIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + <uses-permission android:name="android.permission.RECEIVE_SENSITIVE_NOTIFICATIONS" /> <!-- role holder APIs --> <uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" /> diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index e7ae61072db4..75d3dce55abd 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -16,10 +16,17 @@ package com.android.server.notification; +import static android.Manifest.permission.RECEIVE_SENSITIVE_NOTIFICATIONS; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.NOT_FOREGROUND_SERVICE; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; +import static android.app.Notification.EXTRA_BUILDER_APPLICATION_INFO; +import static android.app.Notification.EXTRA_LARGE_ICON_BIG; +import static android.app.Notification.EXTRA_SUB_TEXT; +import static android.app.Notification.EXTRA_TEXT; +import static android.app.Notification.EXTRA_TEXT_LINES; +import static android.app.Notification.EXTRA_TITLE_BIG; import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; import static android.app.Notification.FLAG_AUTO_CANCEL; import static android.app.Notification.FLAG_BUBBLE; @@ -80,6 +87,7 @@ import static android.os.PowerWhitelistManager.REASON_NOTIFICATION_SERVICE; import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED; import static android.os.UserHandle.USER_NULL; import static android.os.UserHandle.USER_SYSTEM; +import static android.service.notification.Flags.redactSensitiveNotificationsFromUntrustedListeners; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; @@ -163,6 +171,7 @@ import android.app.ITransientNotificationCallback; import android.app.IUriGrantsManager; import android.app.KeyguardManager; import android.app.Notification; +import android.app.Notification.MessagingStyle; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationHistory; @@ -170,6 +179,7 @@ import android.app.NotificationHistory.HistoricalNotification; import android.app.NotificationManager; import android.app.NotificationManager.Policy; import android.app.PendingIntent; +import android.app.Person; import android.app.RemoteServiceException.BadForegroundServiceNotificationException; import android.app.RemoteServiceException.BadUserInitiatedJobNotificationException; import android.app.StatsManager; @@ -575,7 +585,8 @@ public class NotificationManagerService extends SystemService { private ActivityTaskManagerInternal mAtm; private ActivityManager mActivityManager; private ActivityManagerInternal mAmi; - private IPackageManager mPackageManager; + @VisibleForTesting + IPackageManager mPackageManager; private PackageManager mPackageManagerClient; PackageManagerInternal mPackageManagerInternal; private PermissionManager mPermissionManager; @@ -586,7 +597,8 @@ public class NotificationManagerService extends SystemService { @Nullable StatusBarManagerInternal mStatusBar; private WindowManagerInternal mWindowManagerInternal; private AlarmManager mAlarmManager; - private ICompanionDeviceManager mCompanionManager; + @VisibleForTesting + ICompanionDeviceManager mCompanionManager; private AccessibilityManager mAccessibilityManager; private DeviceIdleManager mDeviceIdleManager; private IUriGrantsManager mUgm; @@ -603,7 +615,8 @@ public class NotificationManagerService extends SystemService { private PostNotificationTrackerFactory mPostNotificationTrackerFactory; final IBinder mForegroundToken = new Binder(); - private WorkerHandler mHandler; + @VisibleForTesting + WorkerHandler mHandler; private final HandlerThread mRankingThread = new HandlerThread("ranker", Process.THREAD_PRIORITY_BACKGROUND); @@ -695,7 +708,8 @@ public class NotificationManagerService extends SystemService { private final UserProfiles mUserProfiles = new UserProfiles(); private NotificationListeners mListeners; - private NotificationAssistants mAssistants; + @VisibleForTesting + NotificationAssistants mAssistants; private ConditionProviders mConditionProviders; private NotificationUsageStats mUsageStats; private boolean mLockScreenAllowSecureNotifications = true; @@ -2722,7 +2736,8 @@ public class NotificationManagerService extends SystemService { new NotificationAssistants(getContext(), mNotificationLock, mUserProfiles, AppGlobals.getPackageManager()), new ConditionProviders(getContext(), mUserProfiles, AppGlobals.getPackageManager()), - null, snoozeHelper, new NotificationUsageStats(getContext()), + null /*CDM is not initialized yet*/, snoozeHelper, + new NotificationUsageStats(getContext()), new AtomicFile(new File( systemDir, "notification_policy.xml"), "notification-policy"), (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE), @@ -3402,7 +3417,7 @@ public class NotificationManagerService extends SystemService { private String getHistoryText(Context appContext, Notification n) { CharSequence text = null; if (n.extras != null) { - text = n.extras.getCharSequence(Notification.EXTRA_TEXT); + text = n.extras.getCharSequence(EXTRA_TEXT); Notification.Builder nb = Notification.Builder.recoverBuilder(appContext, n); @@ -3417,7 +3432,7 @@ public class NotificationManagerService extends SystemService { } if (TextUtils.isEmpty(text)) { - text = n.extras.getCharSequence(Notification.EXTRA_TEXT); + text = n.extras.getCharSequence(EXTRA_TEXT); } } return text == null ? null : String.valueOf(text); @@ -5180,20 +5195,14 @@ public class NotificationManagerService extends SystemService { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); final boolean getKeys = keys != null; final int N = getKeys ? keys.length : mNotificationList.size(); - final ArrayList<StatusBarNotification> list - = new ArrayList<StatusBarNotification>(N); + final ArrayList<StatusBarNotification> list = new ArrayList<>(N); for (int i=0; i<N; i++) { final NotificationRecord r = getKeys ? mNotificationsByKey.get(keys[i]) : mNotificationList.get(i); - if (r == null) continue; - StatusBarNotification sbn = r.getSbn(); - if (!isVisibleToListener(sbn, r.getNotificationType(), info)) continue; - StatusBarNotification sbnToSend = - (trim == TRIM_FULL) ? sbn : sbn.cloneLight(); - list.add(sbnToSend); + addToListIfNeeded(r, info, list, trim); } - return new ParceledListSlice<StatusBarNotification>(list); + return new ParceledListSlice<>(list); } } @@ -5215,18 +5224,25 @@ public class NotificationManagerService extends SystemService { final int N = snoozedRecords.size(); final ArrayList<StatusBarNotification> list = new ArrayList<>(N); for (int i=0; i < N; i++) { - final NotificationRecord r = snoozedRecords.get(i); - if (r == null) continue; - StatusBarNotification sbn = r.getSbn(); - if (!isVisibleToListener(sbn, r.getNotificationType(), info)) continue; - StatusBarNotification sbnToSend = - (trim == TRIM_FULL) ? sbn : sbn.cloneLight(); - list.add(sbnToSend); + addToListIfNeeded(snoozedRecords.get(i), info, list, trim); } return new ParceledListSlice<>(list); } } + private void addToListIfNeeded(NotificationRecord r, ManagedServiceInfo info, + ArrayList<StatusBarNotification> notifications, int trim) { + if (r == null) return; + StatusBarNotification sbn = r.getSbn(); + if (!isVisibleToListener(sbn, r.getNotificationType(), info)) return; + if (mListeners.hasSensitiveContent(r) && !mListeners.isUidTrusted(info.uid)) { + notifications.add(mListeners.redactStatusBarNotification(sbn)); + } else { + notifications.add((trim == TRIM_FULL) ? sbn : sbn.cloneLight()); + } + + } + @Override public void clearRequestedListenerHints(INotificationListener token) { final long identity = Binder.clearCallingIdentity(); @@ -8577,8 +8593,8 @@ public class NotificationManagerService extends SystemService { } // Do not compare Spannables (will always return false); compare unstyled Strings - final String oldText = String.valueOf(oldN.extras.get(Notification.EXTRA_TEXT)); - final String newText = String.valueOf(newN.extras.get(Notification.EXTRA_TEXT)); + final String oldText = String.valueOf(oldN.extras.get(EXTRA_TEXT)); + final String newText = String.valueOf(newN.extras.get(EXTRA_TEXT)); if (!Objects.equals(oldText, newText)) { if (DEBUG_INTERRUPTIVENESS) { Slog.v(TAG, "INTERRUPTIVENESS: " @@ -11459,6 +11475,9 @@ public class NotificationManagerService extends SystemService { static final String FLAG_SEPARATOR = "\\|"; private final ArraySet<ManagedServiceInfo> mLightTrimListeners = new ArraySet<>(); + + @GuardedBy("mTrustedListenerUids") + private final ArraySet<Integer> mTrustedListenerUids = new ArraySet<>(); @GuardedBy("mRequestedNotificationListeners") private final ArrayMap<Pair<ComponentName, Integer>, NotificationListenerFilter> mRequestedNotificationListeners = new ArrayMap<>(); @@ -11480,6 +11499,24 @@ public class NotificationManagerService extends SystemService { protected void setPackageOrComponentEnabled(String pkgOrComponent, int userId, boolean isPrimary, boolean enabled, boolean userSet) { super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled, userSet); + String pkgName = getPackageName(pkgOrComponent); + if (redactSensitiveNotificationsFromUntrustedListeners()) { + try { + int uid = mPackageManagerClient.getPackageUidAsUser(pkgName, userId); + if (!enabled) { + synchronized (mTrustedListenerUids) { + mTrustedListenerUids.remove(uid); + } + } + if (enabled && isAppTrustedNotificationListenerService(uid, pkgName)) { + synchronized (mTrustedListenerUids) { + mTrustedListenerUids.add(uid); + } + } + } catch (NameNotFoundException e) { + Slog.e(TAG, "PackageManager could not find package " + pkgName, e); + } + } mContext.sendBroadcastAsUser( new Intent(ACTION_NOTIFICATION_LISTENER_ENABLED_CHANGED) @@ -11557,6 +11594,13 @@ public class NotificationManagerService extends SystemService { update = makeRankingUpdateLocked(info); updateUriPermissionsForActiveNotificationsLocked(info, true); } + if (redactSensitiveNotificationsFromUntrustedListeners() + && isAppTrustedNotificationListenerService( + info.uid, info.component.getPackageName())) { + synchronized (mTrustedListenerUids) { + mTrustedListenerUids.add(info.uid); + } + } try { listener.onListenerConnected(update); } catch (RemoteException e) { @@ -11572,6 +11616,11 @@ public class NotificationManagerService extends SystemService { updateListenerHintsLocked(); updateEffectsSuppressorLocked(); } + if (redactSensitiveNotificationsFromUntrustedListeners()) { + synchronized (mTrustedListenerUids) { + mTrustedListenerUids.remove(removed.uid); + } + } mLightTrimListeners.remove(removed); } @@ -11877,8 +11926,16 @@ public class NotificationManagerService extends SystemService { StatusBarNotification sbn = r.getSbn(); StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null; TrimCache trimCache = new TrimCache(sbn); + TrimCache redactedCache = null; + StatusBarNotification redactedSbn = null; + StatusBarNotification oldRedactedSbn = null; + boolean isNewSensitive = hasSensitiveContent(r); + boolean isOldSensitive = hasSensitiveContent(old); for (final ManagedServiceInfo info : getServices()) { + boolean isTrusted = isUidTrusted(info.uid); + boolean sendRedacted = isNewSensitive && !isTrusted; + boolean sendOldRedacted = isOldSensitive && !isTrusted; boolean sbnVisible = isVisibleToListener(sbn, r.getNotificationType(), info); boolean oldSbnVisible = (oldSbn != null) && isVisibleToListener(oldSbn, old.getNotificationType(), info); @@ -11904,9 +11961,14 @@ public class NotificationManagerService extends SystemService { // This notification became invisible -> remove the old one. if (oldSbnVisible && !sbnVisible) { - final StatusBarNotification oldSbnLightClone = oldSbn.cloneLight(); + if (sendOldRedacted && oldRedactedSbn == null) { + oldRedactedSbn = redactStatusBarNotification(oldSbn); + } + final StatusBarNotification oldSbnLightClone = + sendOldRedacted ? oldRedactedSbn.cloneLight() : oldSbn.cloneLight(); listenerCalls.add(() -> notifyRemoved( info, oldSbnLightClone, update, null, REASON_USER_STOPPED)); + continue; } // Grant access before listener is notified @@ -11920,7 +11982,13 @@ public class NotificationManagerService extends SystemService { sbn.getUid(), false /* direct */, false /* retainOnUpdate */); - final StatusBarNotification sbnToPost = trimCache.ForListener(info); + if (sendRedacted && redactedSbn == null) { + redactedSbn = redactStatusBarNotification(sbn); + redactedCache = new TrimCache(redactedSbn); + } + + final StatusBarNotification sbnToPost = sendRedacted + ? redactedCache.ForListener(info) : trimCache.ForListener(info); listenerCalls.add(() -> notifyPosted(info, sbnToPost, update)); } } catch (Exception e) { @@ -11929,6 +11997,109 @@ public class NotificationManagerService extends SystemService { return listenerCalls; } + boolean isAppTrustedNotificationListenerService(int uid, String pkg) { + if (!redactSensitiveNotificationsFromUntrustedListeners()) { + return true; + } + + try { + if (mPackageManager.checkUidPermission(RECEIVE_SENSITIVE_NOTIFICATIONS, uid) + == PERMISSION_GRANTED || mPackageManagerInternal.isPlatformSigned(pkg)) { + return true; + } + + // check if there is a CDM association with the listener + // We don't listen for changes because if an association is lost, the app loses + // NLS access + List<AssociationInfo> cdmAssocs = new ArrayList<>(); + if (mCompanionManager == null) { + mCompanionManager = getCompanionManager(); + } + if (mCompanionManager != null) { + cdmAssocs = + mCompanionManager.getAllAssociationsForUser(UserHandle.getUserId(uid)); + } + for (int i = 0; i < cdmAssocs.size(); i++) { + AssociationInfo assocInfo = cdmAssocs.get(i); + if (!assocInfo.isRevoked() && pkg.equals(assocInfo.getPackageName()) + && assocInfo.getUserId() == UserHandle.getUserId(uid)) { + return true; + } + } + } catch (RemoteException e) { + Slog.e(TAG, "Failed to check trusted status of listener", e); + } + return false; + } + + StatusBarNotification redactStatusBarNotification(StatusBarNotification sbn) { + if (!redactSensitiveNotificationsFromUntrustedListeners()) { + return sbn; + } + + ApplicationInfo appInfo = sbn.getNotification().extras.getParcelable( + EXTRA_BUILDER_APPLICATION_INFO, ApplicationInfo.class); + String pkgLabel; + if (appInfo != null) { + pkgLabel = appInfo.loadLabel(mPackageManagerClient).toString(); + } else { + Slog.w(TAG, "StatusBarNotification " + sbn + " does not have ApplicationInfo." + + " Did you pass in a 'cloneLight' notification?"); + pkgLabel = sbn.getPackageName(); + } + String redactedText = mContext.getString(R.string.redacted_notification_message); + Notification oldNotif = sbn.getNotification(); + Notification oldClone = new Notification(); + oldNotif.cloneInto(oldClone, false); + Notification.Builder redactedNotifBuilder = + new Notification.Builder(getContext(), oldClone); + redactedNotifBuilder.setContentTitle(pkgLabel); + redactedNotifBuilder.setContentText(redactedText); + redactedNotifBuilder.setSubText(null); + redactedNotifBuilder.setActions(); + if (oldNotif.actions != null) { + for (int i = 0; i < oldNotif.actions.length; i++) { + Notification.Action act = + new Notification.Action.Builder(oldNotif.actions[i]).build(); + act.title = mContext.getString(R.string.redacted_notification_action_title); + redactedNotifBuilder.addAction(act); + } + } + + if (oldNotif.isStyle(MessagingStyle.class)) { + Person empty = new Person.Builder().setName("").build(); + MessagingStyle messageStyle = new MessagingStyle(empty); + messageStyle.addMessage(new MessagingStyle.Message( + redactedText, System.currentTimeMillis(), empty)); + redactedNotifBuilder.setStyle(messageStyle); + } + + Notification redacted = redactedNotifBuilder.build(); + // Notification extras can't always be overridden by a builder (configured by a system + // property), so set them after building + if (redacted.extras.containsKey(EXTRA_TITLE_BIG)) { + redacted.extras.putString(EXTRA_TITLE_BIG, pkgLabel); + } + redacted.extras.remove(EXTRA_SUB_TEXT); + redacted.extras.remove(EXTRA_TEXT_LINES); + redacted.extras.remove(EXTRA_LARGE_ICON_BIG); + return sbn.cloneShallow(redacted); + } + + boolean hasSensitiveContent(NotificationRecord r) { + if (r == null || !redactSensitiveNotificationsFromUntrustedListeners()) { + return false; + } + return r.hasSensitiveContent(); + } + + boolean isUidTrusted(int uid) { + synchronized (mTrustedListenerUids) { + return !redactSensitiveNotificationsFromUntrustedListeners() + || mTrustedListenerUids.contains(uid); + } + } + /** * Synchronously grant or revoke permissions to Uris for all active and visible * notifications to just the NotificationListenerService provided. @@ -11985,6 +12156,8 @@ public class NotificationManagerService extends SystemService { // NOTE: this copy is lightweight: it doesn't include heavyweight parts of the // notification final StatusBarNotification sbnLight = sbn.cloneLight(); + StatusBarNotification redactedSbn = null; + boolean hasSensitiveContent = hasSensitiveContent(r); for (final ManagedServiceInfo info : getServices()) { if (!isVisibleToListener(sbn, r.getNotificationType(), info)) { continue; @@ -12004,11 +12177,18 @@ public class NotificationManagerService extends SystemService { continue; } + boolean sendRedacted = redactSensitiveNotificationsFromUntrustedListeners() + && hasSensitiveContent && !isUidTrusted(info.uid); + if (sendRedacted && redactedSbn == null) { + redactedSbn = redactStatusBarNotification(sbn); + } + // Only assistants can get stats final NotificationStats stats = mAssistants.isServiceTokenValidLocked(info.service) ? notificationStats : null; + final StatusBarNotification sbnToSend = sendRedacted ? redactedSbn : sbnLight; final NotificationRankingUpdate update = makeRankingUpdateLocked(info); - mHandler.post(() -> notifyRemoved(info, sbnLight, update, stats, reason)); + mHandler.post(() -> notifyRemoved(info, sbnToSend, update, stats, reason)); } // Revoke access after all listeners have been updated diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java index ea113957dddd..2868b7e2bd4d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java @@ -15,7 +15,10 @@ */ package com.android.server.notification; +import static android.Manifest.permission.RECEIVE_SENSITIVE_NOTIFICATIONS; import static android.content.pm.PackageManager.MATCH_ANY_USER; +import static android.permission.PermissionManager.PERMISSION_GRANTED; +import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; @@ -47,11 +50,14 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.annotation.SuppressLint; import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; +import android.companion.AssociationInfo; +import android.companion.ICompanionDeviceManager; import android.content.ComponentName; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; @@ -62,6 +68,10 @@ import android.os.Bundle; import android.os.Parcel; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.service.notification.INotificationListener; import android.service.notification.NotificationListenerFilter; import android.service.notification.NotificationListenerService; @@ -76,10 +86,12 @@ import android.util.Xml; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.UiServiceTestCase; +import com.android.server.pm.pkg.PackageStateInternal; import com.google.common.collect.ImmutableList; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentMatcher; import org.mockito.Mock; @@ -90,12 +102,17 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; +@SuppressLint("GuardedBy") public class NotificationListenersTest extends UiServiceTestCase { + @Rule + public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Mock private PackageManager mPm; @Mock @@ -103,7 +120,8 @@ public class NotificationListenersTest extends UiServiceTestCase { @Mock private Resources mResources; - @Mock + // mNm is going to be a spy, so it must use doReturn.when, not when.thenReturn, as + // when.thenReturn will result in the real method being called NotificationManagerService mNm; @Mock private INotificationManager mINm; @@ -111,6 +129,7 @@ public class NotificationListenersTest extends UiServiceTestCase { NotificationManagerService.NotificationListeners mListeners; + private int mUid1 = 98989; private ComponentName mCn1 = new ComponentName("pkg", "pkg.cmp"); private ComponentName mCn2 = new ComponentName("pkg2", "pkg2.cmp2"); private ComponentName mUninstalledComponent = new ComponentName("pkg3", @@ -118,15 +137,26 @@ public class NotificationListenersTest extends UiServiceTestCase { @Before public void setUp() throws Exception { + mNm = spy(new NotificationManagerService(mContext)); MockitoAnnotations.initMocks(this); getContext().setMockPackageManager(mPm); doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any()); - when(mNm.isInteractionVisibleToListener(any(), anyInt())).thenReturn(true); + doReturn(true).when(mNm).isInteractionVisibleToListener(any(), anyInt()); mListeners = spy(mNm.new NotificationListeners( mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm)); when(mNm.getBinderService()).thenReturn(mINm); + mNm.mPackageManager = mock(IPackageManager.class); + PackageStateInternal psi = mock(PackageStateInternal.class); + mNm.mPackageManagerInternal = mPmi; + when(psi.getAppId()).thenReturn(mUid1); + when(mNm.mPackageManagerInternal.getPackageStateInternal(any())).thenReturn(psi); + mNm.mCompanionManager = mock(ICompanionDeviceManager.class); + when(mNm.mCompanionManager.getAllAssociationsForUser(anyInt())) + .thenReturn(new ArrayList<>()); + mNm.mHandler = mock(NotificationManagerService.WorkerHandler.class); + mNm.mAssistants = mock(NotificationManagerService.NotificationAssistants.class); } @Test @@ -499,11 +529,11 @@ public class NotificationListenersTest extends UiServiceTestCase { // Neither user0 and user1 is in the lockdown mode when(r0.getUser()).thenReturn(uh0); when(uh0.getIdentifier()).thenReturn(0); - when(mNm.isInLockDownMode(0)).thenReturn(false); + doReturn(false).when(mNm).isInLockDownMode(0); when(r1.getUser()).thenReturn(uh1); when(uh1.getIdentifier()).thenReturn(1); - when(mNm.isInLockDownMode(1)).thenReturn(false); + doReturn(false).when(mNm).isInLockDownMode(1); mListeners.notifyPostedLocked(r0, old0, true); mListeners.notifyPostedLocked(r0, old0, false); @@ -555,12 +585,12 @@ public class NotificationListenersTest extends UiServiceTestCase { // Neither user0 and user1 is in the lockdown mode when(r0.getUser()).thenReturn(uh0); when(uh0.getIdentifier()).thenReturn(0); - when(mNm.isInLockDownMode(0)).thenReturn(false); + doReturn(false).when(mNm).isInLockDownMode(0); when(r0.getSbn()).thenReturn(sbn); when(r1.getUser()).thenReturn(uh1); when(uh1.getIdentifier()).thenReturn(1); - when(mNm.isInLockDownMode(1)).thenReturn(false); + doReturn(false).when(mNm).isInLockDownMode(1); when(r1.getSbn()).thenReturn(sbn); mListeners.notifyRemovedLocked(r0, 0, rs0); @@ -617,9 +647,10 @@ public class NotificationListenersTest extends UiServiceTestCase { List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(info); when(mListeners.getServices()).thenReturn(services); - when(mNm.isVisibleToListener(any(), anyInt(), any())).thenReturn(true); - when(mNm.makeRankingUpdateLocked(info)).thenReturn(mock(NotificationRankingUpdate.class)); - mNm.mPackageManagerInternal = mPmi; + doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any()); + doReturn(mock(NotificationRankingUpdate.class)).when(mNm).makeRankingUpdateLocked(info); + doReturn(false).when(mNm).isInLockDownMode(anyInt()); + doNothing().when(mNm).updateUriPermissions(any(), any(), any(), anyInt()); mListeners.notifyPostedLocked(r, null); @@ -664,6 +695,143 @@ public class NotificationListenersTest extends UiServiceTestCase { }, 20, 50); } + @Test + @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS) + public void testListenerTrusted_withPermission() throws RemoteException { + when(mNm.mPackageManager.checkUidPermission(RECEIVE_SENSITIVE_NOTIFICATIONS, mUid1)) + .thenReturn(PERMISSION_GRANTED); + ManagedServices.ManagedServiceInfo info = getMockServiceInfo(); + mListeners.onServiceAdded(info); + assertTrue(mListeners.isUidTrusted(mUid1)); + } + + @Test + @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS) + public void testListenerTrusted_withSystemSignature() { + when(mNm.mPackageManagerInternal.isPlatformSigned(mCn1.getPackageName())).thenReturn(true); + ManagedServices.ManagedServiceInfo info = getMockServiceInfo(); + mListeners.onServiceAdded(info); + assertTrue(mListeners.isUidTrusted(mUid1)); + } + + @Test + @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS) + public void testListenerTrusted_withCdmAssociation() throws Exception { + mNm.mCompanionManager = mock(ICompanionDeviceManager.class); + AssociationInfo assocInfo = mock(AssociationInfo.class); + when(assocInfo.isRevoked()).thenReturn(false); + when(assocInfo.getPackageName()).thenReturn(mCn1.getPackageName()); + when(assocInfo.getUserId()).thenReturn(UserHandle.getUserId(mUid1)); + ArrayList<AssociationInfo> infos = new ArrayList<>(); + infos.add(assocInfo); + when(mNm.mCompanionManager.getAllAssociationsForUser(anyInt())).thenReturn(infos); + ManagedServices.ManagedServiceInfo info = getMockServiceInfo(); + mListeners.onServiceAdded(info); + assertTrue(mListeners.isUidTrusted(mUid1)); + } + + @Test + @RequiresFlagsDisabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS) + public void testListenerTrusted_ifFlagDisabled() { + ManagedServices.ManagedServiceInfo info = getMockServiceInfo(); + mListeners.onServiceAdded(info); + assertTrue(mListeners.isUidTrusted(mUid1)); + } + + @Test + @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS) + public void testRedaction_whenPosted() { + ArrayList<ManagedServices.ManagedServiceInfo> infos = new ArrayList<>(); + infos.add(getMockServiceInfo()); + doReturn(infos).when(mListeners).getServices(); + doReturn(mock(StatusBarNotification.class)) + .when(mListeners).redactStatusBarNotification(any()); + doReturn(false).when(mNm).isInLockDownMode(anyInt()); + doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any()); + NotificationRecord r = mock(NotificationRecord.class); + when(r.getUser()).thenReturn(UserHandle.of(0)); + StatusBarNotification sbn = getSbn(0); + NotificationRecord old = mock(NotificationRecord.class); + when(old.getUser()).thenReturn(UserHandle.of(0)); + StatusBarNotification oldSbn = getSbn(1); + when(r.getSbn()).thenReturn(sbn); + when(r.hasSensitiveContent()).thenReturn(true); + when(old.getSbn()).thenReturn(oldSbn); + when(old.hasSensitiveContent()).thenReturn(true); + + mListeners.notifyPostedLocked(r, old); + verify(mListeners, atLeast(1)).redactStatusBarNotification(eq(sbn)); + verify(mListeners, never()).redactStatusBarNotification(eq(oldSbn)); + + + } + + @Test + @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS) + public void testRedaction_whenPosted_oldRemoved() { + ArrayList<ManagedServices.ManagedServiceInfo> infos = new ArrayList<>(); + infos.add(getMockServiceInfo()); + doReturn(infos).when(mListeners).getServices(); + doReturn(mock(StatusBarNotification.class)) + .when(mListeners).redactStatusBarNotification(any()); + doReturn(false).when(mNm).isInLockDownMode(anyInt()); + doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any()); + NotificationRecord r = mock(NotificationRecord.class); + when(r.getUser()).thenReturn(UserHandle.of(0)); + StatusBarNotification sbn = getSbn(0); + NotificationRecord old = mock(NotificationRecord.class); + when(old.getUser()).thenReturn(UserHandle.of(0)); + StatusBarNotification oldSbn = getSbn(1); + when(r.getSbn()).thenReturn(sbn); + when(r.hasSensitiveContent()).thenReturn(true); + when(old.getSbn()).thenReturn(oldSbn); + when(old.hasSensitiveContent()).thenReturn(true); + + doReturn(true).when(mNm).isVisibleToListener(eq(oldSbn), anyInt(), any()); + doReturn(false).when(mNm).isVisibleToListener(eq(sbn), anyInt(), any()); + mListeners.notifyPostedLocked(r, old); + // When the old sbn is removed, the old should be redacted + verify(mListeners, atLeast(1)).redactStatusBarNotification(eq(oldSbn)); + } + + @Test + @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS) + public void testRedaction_whenRemoved() { + doReturn(mock(StatusBarNotification.class)) + .when(mListeners).redactStatusBarNotification(any()); + ArrayList<ManagedServices.ManagedServiceInfo> infos = new ArrayList<>(); + infos.add(getMockServiceInfo()); + doReturn(infos).when(mListeners).getServices(); + doReturn(false).when(mNm).isInLockDownMode(anyInt()); + doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any()); + NotificationRecord r = mock(NotificationRecord.class); + when(r.getUser()).thenReturn(UserHandle.of(0)); + StatusBarNotification sbn = getSbn(0); + when(r.getSbn()).thenReturn(sbn); + when(r.hasSensitiveContent()).thenReturn(true); + mNm.mAssistants = mock(NotificationManagerService.NotificationAssistants.class); + + mListeners.notifyRemovedLocked(r, 0, mock(NotificationStats.class)); + verify(mListeners, atLeast(1)).redactStatusBarNotification(any()); + } + + @Test + @RequiresFlagsDisabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS) + public void testRedaction_noneIfFlagDisabled() { + ArrayList<ManagedServices.ManagedServiceInfo> infos = new ArrayList<>(); + infos.add(getMockServiceInfo()); + doReturn(infos).when(mListeners).getServices(); + doReturn(false).when(mNm).isInLockDownMode(anyInt()); + doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any()); + NotificationRecord r = mock(NotificationRecord.class); + when(r.getUser()).thenReturn(UserHandle.of(0)); + StatusBarNotification sbn = getSbn(0); + when(r.getSbn()).thenReturn(sbn); + when(r.hasSensitiveContent()).thenReturn(true); + mListeners.notifyRemovedLocked(r, 0, mock(NotificationStats.class)); + verify(mListeners, never()).redactStatusBarNotification(eq(sbn)); + } + /** * Helper method to test the thread safety of some operations. * @@ -701,10 +869,8 @@ public class NotificationListenersTest extends UiServiceTestCase { private ManagedServices.ManagedServiceInfo getParcelingListener( final NotificationChannelGroup toParcel) throws RemoteException { - ManagedServices.ManagedServiceInfo i1 = mock(ManagedServices.ManagedServiceInfo.class); - when(i1.isSystem()).thenReturn(true); - INotificationListener l1 = mock(INotificationListener.class); - when(i1.enabledAndUserMatches(anyInt())).thenReturn(true); + ManagedServices.ManagedServiceInfo i1 = getMockServiceInfo(); + INotificationListener l1 = (INotificationListener) i1.getService(); doAnswer(invocationOnMock -> { try { toParcel.writeToParcel(Parcel.obtain(), 0); @@ -715,7 +881,24 @@ public class NotificationListenersTest extends UiServiceTestCase { } return null; }).when(l1).onNotificationChannelGroupModification(anyString(), any(), any(), anyInt()); + return i1; + } + + private ManagedServices.ManagedServiceInfo getMockServiceInfo() { + ManagedServices.ManagedServiceInfo i1 = mock(ManagedServices.ManagedServiceInfo.class); + when(i1.isSystem()).thenReturn(true); + INotificationListener l1 = mock(INotificationListener.class); + when(i1.enabledAndUserMatches(anyInt())).thenReturn(true); when(i1.getService()).thenReturn(l1); + i1.service = l1; + i1.uid = mUid1; + i1.component = mCn1; return i1; } + + private StatusBarNotification getSbn(int id) { + return new StatusBarNotification("pkg1", "pkg1", id, "", mUid1, 0, + mock(Notification.class), UserHandle.of(0), "", 0); + + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 776189eeb7c3..48c00a86fd17 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -24,6 +24,7 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.Notification.EXTRA_ALLOW_DURING_SETUP; import static android.app.Notification.EXTRA_PICTURE; import static android.app.Notification.EXTRA_PICTURE_ICON; +import static android.app.Notification.EXTRA_TEXT; import static android.app.Notification.FLAG_AUTO_CANCEL; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_CAN_COLORIZE; @@ -77,6 +78,7 @@ import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.service.notification.Adjustment.KEY_IMPORTANCE; import static android.service.notification.Adjustment.KEY_USER_SENTIMENT; +import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; @@ -210,6 +212,9 @@ import android.os.UserManager; import android.os.WorkSource; import android.permission.PermissionManager; import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.rule.DeniedDevices; import android.platform.test.rule.DeviceProduct; @@ -220,6 +225,7 @@ import android.provider.Settings; import android.service.notification.Adjustment; import android.service.notification.ConversationChannelWrapper; import android.service.notification.DeviceEffectsApplier; +import android.service.notification.INotificationListener; import android.service.notification.NotificationListenerFilter; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationRankingUpdate; @@ -333,6 +339,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManagerService.class.getSimpleName() + ".TIMEOUT"; private static final String EXTRA_KEY = "key"; private static final String SCHEME_TIMEOUT = "timeout"; + private static final String REDACTED_TEXT = "redacted text"; private final int mUid = Binder.getCallingUid(); private final @UserIdInt int mUserId = UserHandle.getUserId(mUid); @@ -343,6 +350,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); + @Rule + public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private TestableNotificationManagerService mService; private INotificationManager mBinderService; private NotificationManagerInternal mInternalService; @@ -1015,7 +1025,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .setSmallIcon(android.R.drawable.sym_def_app_icon); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, id, "tag", mUid, 0, nb.build(), new UserHandle(userId), null, 0); - return new NotificationRecord(mContext, sbn, channel); + NotificationRecord r = new NotificationRecord(mContext, sbn, channel); + return r; } private NotificationRecord generateMessageBubbleNotifRecord(NotificationChannel channel, @@ -1038,6 +1049,16 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { return new NotificationRecord(mContext, sbn, channel); } + private StatusBarNotification generateRedactedSbn(NotificationChannel channel, int id, + int userId) { + Notification.Builder nb = new Notification.Builder(mContext, channel.getId()) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText(REDACTED_TEXT); + return new StatusBarNotification(PKG, PKG, id, "tag", mUid, 0, + nb.build(), new UserHandle(userId), null, 0); + } + private Map<String, Answer> getSignalExtractorSideEffects() { Map<String, Answer> answers = new ArrayMap<>(); @@ -11514,6 +11535,67 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS) + public void testGetActiveNotificationsFromListener_redactNotification() throws Exception { + NotificationRecord r = + generateNotificationRecord(mTestNotificationChannel, 0, 0); + mService.addNotification(r); + when(mListeners.isUidTrusted(anyInt())).thenReturn(false); + when(mListeners.hasSensitiveContent(any())).thenReturn(true); + StatusBarNotification redacted = generateRedactedSbn(mTestNotificationChannel, 1, 1); + when(mListeners.redactStatusBarNotification(any())).thenReturn(redacted); + ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class); + info.userid = 0; + when(info.isSameUser(anyInt())).thenReturn(true); + when(info.enabledAndUserMatches(anyInt())).thenReturn(true); + when(mListeners.checkServiceTokenLocked(any())).thenReturn(info); + List<StatusBarNotification> notifications = mBinderService + .getActiveNotificationsFromListener(mock(INotificationListener.class), null, -1) + .getList(); + + boolean foundRedactedSbn = false; + for (StatusBarNotification sbn: notifications) { + String text = sbn.getNotification().extras.getCharSequence(EXTRA_TEXT).toString(); + if (REDACTED_TEXT.equals(text)) { + foundRedactedSbn = true; + break; + } + } + assertTrue("expect to find a redacted notification", foundRedactedSbn); + } + + @Test + @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS) + public void testGetSnoozedNotificationsFromListener_redactNotification() throws Exception { + NotificationRecord r = + generateNotificationRecord(mTestNotificationChannel, 0, 0); + mService.addNotification(r); + mService.snoozeNotificationInt(r.getKey(), 1000, null, mListener); + when(mListeners.isUidTrusted(anyInt())).thenReturn(false); + when(mListeners.hasSensitiveContent(any())).thenReturn(true); + StatusBarNotification redacted = generateRedactedSbn(mTestNotificationChannel, 1, 1); + when(mListeners.redactStatusBarNotification(any())).thenReturn(redacted); + ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class); + info.userid = 0; + when(info.isSameUser(anyInt())).thenReturn(true); + when(info.enabledAndUserMatches(anyInt())).thenReturn(true); + when(mListeners.checkServiceTokenLocked(any())).thenReturn(info); + List<StatusBarNotification> notifications = mBinderService + .getSnoozedNotificationsFromListener(mock(INotificationListener.class), -1) + .getList(); + + boolean foundRedactedSbn = false; + for (StatusBarNotification sbn: notifications) { + String text = sbn.getNotification().extras.getCharSequence(EXTRA_TEXT).toString(); + if (REDACTED_TEXT.equals(text)) { + foundRedactedSbn = true; + break; + } + } + assertTrue("expect to find a redacted notification", foundRedactedSbn); + } + + @Test public void testUngroupingOngoingAutoSummary() throws Exception { NotificationRecord nr0 = generateNotificationRecord(mTestNotificationChannel, 0); |