Restrict sensitive notifications from untrusted listeners

Redact notifications containing sensitive information to listeners
that are not trusted

Bug: 301960090
Bug: 313709930
Flag: ACONFIG android.service.notification.redact_sensitive_notifications_from_untrusted_listeners DISABLED
Test: atest SensitiveNotificationRedactionTest
Change-Id: I60e2810da2abc3b2e730904599798f7f87eb7ed8
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index a0c5c2c..3fc9709 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -303,6 +303,7 @@
     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 bb56939..264b53c 100644
--- a/core/java/android/service/notification/StatusBarNotification.java
+++ b/core/java/android/service/notification/StatusBarNotification.java
@@ -272,8 +272,10 @@
     /**
      * @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 2a05c84..a2ade6a 100644
--- a/core/java/android/service/notification/flags.aconfig
+++ b/core/java/android/service/notification/flags.aconfig
@@ -15,3 +15,9 @@
     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 c6a241f..0264fdc 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 4596ca7..d2fb9e1 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6363,4 +6363,10 @@
     <!-- 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 ef272ee..3894330 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 bacab0f..6e65c16 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 f1029a3..1a35f04 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 e7ae610..75d3dce 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.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.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.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 @@
     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 @@
     @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 @@
     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 @@
 
     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 @@
                 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 @@
     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 @@
             }
 
             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 @@
                 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 @@
                 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 @@
         }
 
         // 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 @@
         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 @@
         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 @@
                 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 @@
                 updateListenerHintsLocked();
                 updateEffectsSuppressorLocked();
             }
+            if (redactSensitiveNotificationsFromUntrustedListeners()) {
+                synchronized (mTrustedListenerUids) {
+                    mTrustedListenerUids.remove(removed.uid);
+                }
+            }
             mLightTrimListeners.remove(removed);
         }
 
@@ -11877,8 +11926,16 @@
                 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 @@
 
                     // 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 @@
                             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 @@
             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 @@
             // 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 @@
                     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 ea11395..2868b7e 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.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.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 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.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 @@
     @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 @@
 
     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 @@
 
     @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 @@
         // 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 @@
         // 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 @@
         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 @@
         }, 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 @@
     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 @@
             }
             return null;
         }).when(l1).onNotificationChannelGroupModification(anyString(), any(), any(), anyInt());
-        when(i1.getService()).thenReturn(l1);
         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 776189e..48c00a8 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.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.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.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.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 @@
             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 @@
     @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 @@
                 .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 @@
         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 @@
     }
 
     @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);