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);