Merge "Validate pending intents"
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 269bd20..23798c0 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -421,6 +421,7 @@
private IActivityManager mAm;
private ActivityTaskManagerInternal mAtm;
private ActivityManager mActivityManager;
+ private ActivityManagerInternal mAmi;
private IPackageManager mPackageManager;
private PackageManager mPackageManagerClient;
AudioManager mAudioManager;
@@ -1897,7 +1898,7 @@
DevicePolicyManagerInternal dpm, IUriGrantsManager ugm,
UriGrantsManagerInternal ugmInternal, AppOpsManager appOps, UserManager userManager,
NotificationHistoryManager historyManager, StatsManager statsManager,
- TelephonyManager telephonyManager) {
+ TelephonyManager telephonyManager, ActivityManagerInternal ami) {
mHandler = handler;
Resources resources = getContext().getResources();
mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(),
@@ -1919,6 +1920,7 @@
mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
mCompanionManager = companionManager;
mActivityManager = activityManager;
+ mAmi = ami;
mDeviceIdleManager = getContext().getSystemService(DeviceIdleManager.class);
mDpm = dpm;
mUm = userManager;
@@ -2197,7 +2199,8 @@
new NotificationHistoryManager(getContext(), handler),
mStatsManager = (StatsManager) getContext().getSystemService(
Context.STATS_MANAGER),
- getContext().getSystemService(TelephonyManager.class));
+ getContext().getSystemService(TelephonyManager.class),
+ LocalServices.getService(ActivityManagerInternal.class));
publishBinderService(Context.NOTIFICATION_SERVICE, mService, /* allowIsolated= */ false,
DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL);
@@ -5239,8 +5242,8 @@
if (!summaries.containsKey(pkg)) {
// Add summary
final ApplicationInfo appInfo =
- adjustedSbn.getNotification().extras.getParcelable(
- Notification.EXTRA_BUILDER_APPLICATION_INFO);
+ adjustedSbn.getNotification().extras.getParcelable(
+ Notification.EXTRA_BUILDER_APPLICATION_INFO);
final Bundle extras = new Bundle();
extras.putParcelable(Notification.EXTRA_BUILDER_APPLICATION_INFO, appInfo);
final String channelId = notificationRecord.getChannel().getId();
@@ -5276,11 +5279,11 @@
notificationRecord.getIsAppImportanceLocked());
summaries.put(pkg, summarySbn.getKey());
}
- }
- if (summaryRecord != null && checkDisqualifyingFeatures(userId, MY_UID,
- summaryRecord.getSbn().getId(), summaryRecord.getSbn().getTag(), summaryRecord,
- true)) {
- mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord, isAppForeground));
+ if (summaryRecord != null && checkDisqualifyingFeatures(userId, MY_UID,
+ summaryRecord.getSbn().getId(), summaryRecord.getSbn().getTag(), summaryRecord,
+ true)) {
+ mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord, isAppForeground));
+ }
}
}
@@ -6019,13 +6022,17 @@
+ " cannot post for pkg " + targetPkg + " in user " + userId);
}
+ public boolean hasFlag(final int flags, final int flag) {
+ return (flags & flag) != 0;
+ }
/**
* Checks if a notification can be posted. checks rate limiter, snooze helper, and blocking.
*
* Has side effects.
*/
- private boolean checkDisqualifyingFeatures(int userId, int uid, int id, String tag,
+ boolean checkDisqualifyingFeatures(int userId, int uid, int id, String tag,
NotificationRecord r, boolean isAutogroup) {
+ Notification n = r.getNotification();
final String pkg = r.getSbn().getPackageName();
final boolean isSystemNotification =
isUidSystemOrPhone(uid) || ("android".equals(pkg));
@@ -6034,71 +6041,101 @@
// Limit the number of notifications that any given package except the android
// package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks.
if (!isSystemNotification && !isNotificationFromListener) {
- synchronized (mNotificationLock) {
- final int callingUid = Binder.getCallingUid();
- if (mNotificationsByKey.get(r.getSbn().getKey()) == null
- && isCallerInstantApp(callingUid, userId)) {
- // Ephemeral apps have some special constraints for notifications.
- // They are not allowed to create new notifications however they are allowed to
- // update notifications created by the system (e.g. a foreground service
- // notification).
- throw new SecurityException("Instant app " + pkg
- + " cannot create notifications");
- }
+ final int callingUid = Binder.getCallingUid();
+ if (mNotificationsByKey.get(r.getSbn().getKey()) == null
+ && isCallerInstantApp(callingUid, userId)) {
+ // Ephemeral apps have some special constraints for notifications.
+ // They are not allowed to create new notifications however they are allowed to
+ // update notifications created by the system (e.g. a foreground service
+ // notification).
+ throw new SecurityException("Instant app " + pkg
+ + " cannot create notifications");
+ }
- // rate limit updates that aren't completed progress notifications
- if (mNotificationsByKey.get(r.getSbn().getKey()) != null
- && !r.getNotification().hasCompletedProgress()
- && !isAutogroup) {
+ // rate limit updates that aren't completed progress notifications
+ if (mNotificationsByKey.get(r.getSbn().getKey()) != null
+ && !r.getNotification().hasCompletedProgress()
+ && !isAutogroup) {
- final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
- if (appEnqueueRate > mMaxPackageEnqueueRate) {
- mUsageStats.registerOverRateQuota(pkg);
- final long now = SystemClock.elapsedRealtime();
- if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) {
- Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate
- + ". Shedding " + r.getSbn().getKey() + ". package=" + pkg);
- mLastOverRateLogTime = now;
- }
- return false;
+ final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
+ if (appEnqueueRate > mMaxPackageEnqueueRate) {
+ mUsageStats.registerOverRateQuota(pkg);
+ final long now = SystemClock.elapsedRealtime();
+ if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) {
+ Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate
+ + ". Shedding " + r.getSbn().getKey() + ". package=" + pkg);
+ mLastOverRateLogTime = now;
}
+ return false;
}
+ }
- // limit the number of non-fgs outstanding notificationrecords an app can have
- if (!r.getNotification().isForegroundService()) {
- int count = getNotificationCountLocked(pkg, userId, id, tag);
- if (count >= MAX_PACKAGE_NOTIFICATIONS) {
- mUsageStats.registerOverCountQuota(pkg);
- Slog.e(TAG, "Package has already posted or enqueued " + count
- + " notifications. Not showing more. package=" + pkg);
- return false;
- }
+ // limit the number of non-fgs outstanding notificationrecords an app can have
+ if (!n.isForegroundService()) {
+ int count = getNotificationCountLocked(pkg, userId, id, tag);
+ if (count >= MAX_PACKAGE_NOTIFICATIONS) {
+ mUsageStats.registerOverCountQuota(pkg);
+ Slog.e(TAG, "Package has already posted or enqueued " + count
+ + " notifications. Not showing more. package=" + pkg);
+ return false;
}
}
}
- synchronized (mNotificationLock) {
- // snoozed apps
- if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) {
- MetricsLogger.action(r.getLogMaker()
- .setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
- .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED));
- mNotificationRecordLogger.log(
- NotificationRecordLogger.NotificationEvent.NOTIFICATION_NOT_POSTED_SNOOZED,
- r);
- if (DBG) {
- Slog.d(TAG, "Ignored enqueue for snoozed notification " + r.getKey());
+ // bubble or inline reply that's immutable?
+ if (n.getBubbleMetadata() != null
+ && n.getBubbleMetadata().getIntent() != null
+ && hasFlag(mAmi.getPendingIntentFlags(
+ n.getBubbleMetadata().getIntent().getTarget()),
+ PendingIntent.FLAG_IMMUTABLE)) {
+ throw new IllegalArgumentException(r.getKey() + " Not posted."
+ + " PendingIntents attached to bubbles must be mutable");
+ }
+
+ if (n.actions != null) {
+ for (Notification.Action action : n.actions) {
+ if ((action.getRemoteInputs() != null || action.getDataOnlyRemoteInputs() != null)
+ && hasFlag(mAmi.getPendingIntentFlags(action.actionIntent.getTarget()),
+ PendingIntent.FLAG_IMMUTABLE)) {
+ throw new IllegalArgumentException(r.getKey() + " Not posted."
+ + " PendingIntents attached to actions with remote"
+ + " inputs must be mutable");
}
- mSnoozeHelper.update(userId, r);
- handleSavePolicyFile();
- return false;
}
+ }
+
+ if (r.getSystemGeneratedSmartActions() != null) {
+ for (Notification.Action action : r.getSystemGeneratedSmartActions()) {
+ if ((action.getRemoteInputs() != null || action.getDataOnlyRemoteInputs() != null)
+ && hasFlag(mAmi.getPendingIntentFlags(action.actionIntent.getTarget()),
+ PendingIntent.FLAG_IMMUTABLE)) {
+ throw new IllegalArgumentException(r.getKey() + " Not posted."
+ + " PendingIntents attached to contextual actions with remote inputs"
+ + " must be mutable");
+ }
+ }
+ }
+
+ // snoozed apps
+ if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) {
+ MetricsLogger.action(r.getLogMaker()
+ .setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
+ .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED));
+ mNotificationRecordLogger.log(
+ NotificationRecordLogger.NotificationEvent.NOTIFICATION_NOT_POSTED_SNOOZED,
+ r);
+ if (DBG) {
+ Slog.d(TAG, "Ignored enqueue for snoozed notification " + r.getKey());
+ }
+ mSnoozeHelper.update(userId, r);
+ handleSavePolicyFile();
+ return false;
+ }
- // blocked apps
- if (isBlocked(r, mUsageStats)) {
- return false;
- }
+ // blocked apps
+ if (isBlocked(r, mUsageStats)) {
+ return false;
}
return true;
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 ed6a20b..6ee5831 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -43,6 +43,9 @@
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+import static android.app.PendingIntent.FLAG_MUTABLE;
+import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
import static android.content.pm.PackageManager.FEATURE_WATCH;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
@@ -110,6 +113,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.IIntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
@@ -248,6 +252,11 @@
Resources mResources;
@Mock
RankingHandler mRankingHandler;
+ @Mock
+ ActivityManagerInternal mAmi;
+
+ @Mock
+ IIntentSender pi1;
private static final int MAX_POST_DELAY = 1000;
@@ -392,7 +401,6 @@
DeviceIdleInternal deviceIdleInternal = mock(DeviceIdleInternal.class);
when(deviceIdleInternal.getNotificationAllowlistDuration()).thenReturn(3000L);
- ActivityManagerInternal activityManagerInternal = mock(ActivityManagerInternal.class);
LocalServices.removeServiceForTest(UriGrantsManagerInternal.class);
LocalServices.addService(UriGrantsManagerInternal.class, mUgmInternal);
@@ -403,7 +411,7 @@
LocalServices.removeServiceForTest(DeviceIdleInternal.class);
LocalServices.addService(DeviceIdleInternal.class, deviceIdleInternal);
LocalServices.removeServiceForTest(ActivityManagerInternal.class);
- LocalServices.addService(ActivityManagerInternal.class, activityManagerInternal);
+ LocalServices.addService(ActivityManagerInternal.class, mAmi);
doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any());
@@ -477,7 +485,7 @@
mGroupHelper, mAm, mAtm, mAppUsageStats,
mock(DevicePolicyManagerInternal.class), mUgm, mUgmInternal,
mAppOpsManager, mUm, mHistoryManager, mStatsManager,
- mock(TelephonyManager.class));
+ mock(TelephonyManager.class), mAmi);
mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
mService.setAudioManager(mAudioManager);
@@ -674,7 +682,8 @@
}
Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
.setContentTitle("foo")
- .setSmallIcon(android.R.drawable.sym_def_app_icon);
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .addAction(new Notification.Action.Builder(null, "test", null).build());
if (extender != null) {
nb.extend(extender);
}
@@ -810,6 +819,7 @@
PendingIntent pendingIntent = mock(PendingIntent.class);
Intent intent = mock(Intent.class);
when(pendingIntent.getIntent()).thenReturn(intent);
+ when(pendingIntent.getTarget()).thenReturn(pi1);
ActivityInfo info = new ActivityInfo();
info.resizeMode = RESIZE_MODE_RESIZEABLE;
@@ -7134,4 +7144,159 @@
inOrder.verify(parent).recordDismissalSentiment(anyInt());
inOrder.verify(child).recordDismissalSentiment(anyInt());
}
+
+ @Test
+ public void testImmutableBubbleIntent() throws Exception {
+ when(mAmi.getPendingIntentFlags(pi1))
+ .thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT);
+ NotificationRecord r = generateMessageBubbleNotifRecord(true,
+ mTestNotificationChannel, 7, "testImmutableBubbleIntent", null, false);
+ try {
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, r.getSbn().getTag(),
+ r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId());
+
+ waitForIdle();
+ fail("Allowed a bubble with an immutable intent to be posted");
+ } catch (IllegalArgumentException e) {
+ // good
+ }
+ }
+
+ @Test
+ public void testMutableBubbleIntent() throws Exception {
+ when(mAmi.getPendingIntentFlags(pi1))
+ .thenReturn(FLAG_MUTABLE | FLAG_ONE_SHOT);
+ NotificationRecord r = generateMessageBubbleNotifRecord(true,
+ mTestNotificationChannel, 7, "testMutableBubbleIntent", null, false);
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, r.getSbn().getTag(),
+ r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId());
+
+ waitForIdle();
+ StatusBarNotification[] notifs =
+ mBinderService.getActiveNotifications(r.getSbn().getPackageName());
+ assertEquals(1, notifs.length);
+ }
+
+ @Test
+ public void testImmutableDirectReplyActionIntent() throws Exception {
+ when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
+ .thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT);
+ NotificationRecord r = generateMessageBubbleNotifRecord(false,
+ mTestNotificationChannel, 7, "testImmutableDirectReplyActionIntent", null, false);
+ try {
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, r.getSbn().getTag(),
+ r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId());
+
+ waitForIdle();
+ fail("Allowed a direct reply with an immutable intent to be posted");
+ } catch (IllegalArgumentException e) {
+ // good
+ }
+ }
+
+ @Test
+ public void testMutableDirectReplyActionIntent() throws Exception {
+ when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
+ .thenReturn(FLAG_MUTABLE | FLAG_ONE_SHOT);
+ NotificationRecord r = generateMessageBubbleNotifRecord(false,
+ mTestNotificationChannel, 7, "testMutableDirectReplyActionIntent", null, false);
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, r.getSbn().getTag(),
+ r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId());
+
+ waitForIdle();
+ StatusBarNotification[] notifs =
+ mBinderService.getActiveNotifications(r.getSbn().getPackageName());
+ assertEquals(1, notifs.length);
+ }
+
+ @Test
+ public void testImmutableDirectReplyContextualActionIntent() throws Exception {
+ when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
+ .thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT);
+ when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true);
+
+ NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+ ArrayList<Notification.Action> extraAction = new ArrayList<>();
+ RemoteInput remoteInput = new RemoteInput.Builder("reply_key").setLabel("reply").build();
+ PendingIntent inputIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+ Icon icon = Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon);
+ Notification.Action replyAction = new Notification.Action.Builder(icon, "Reply",
+ inputIntent).addRemoteInput(remoteInput)
+ .build();
+ extraAction.add(replyAction);
+ Bundle signals = new Bundle();
+ signals.putParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS, extraAction);
+ Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, "",
+ r.getUser());
+ r.addAdjustment(adjustment);
+ r.applyAdjustments();
+
+ try {
+ mService.checkDisqualifyingFeatures(r.getUserId(), r.getUid(), r.getSbn().getId(),
+ r.getSbn().getTag(), r,false);
+ fail("Allowed a contextual direct reply with an immutable intent to be posted");
+ } catch (IllegalArgumentException e) {
+ // good
+ }
+ }
+
+ @Test
+ public void testMutableDirectReplyContextualActionIntent() throws Exception {
+ when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
+ .thenReturn(FLAG_MUTABLE | FLAG_ONE_SHOT);
+ when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true);
+ NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+ ArrayList<Notification.Action> extraAction = new ArrayList<>();
+ RemoteInput remoteInput = new RemoteInput.Builder("reply_key").setLabel("reply").build();
+ PendingIntent inputIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+ Icon icon = Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon);
+ Notification.Action replyAction = new Notification.Action.Builder(icon, "Reply",
+ inputIntent).addRemoteInput(remoteInput)
+ .build();
+ extraAction.add(replyAction);
+ Bundle signals = new Bundle();
+ signals.putParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS, extraAction);
+ Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, "",
+ r.getUser());
+ r.addAdjustment(adjustment);
+ r.applyAdjustments();
+
+ mService.checkDisqualifyingFeatures(r.getUserId(), r.getUid(), r.getSbn().getId(),
+ r.getSbn().getTag(), r,false);
+ }
+
+ @Test
+ public void testImmutableActionIntent() throws Exception {
+ when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
+ .thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT);
+ NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, r.getSbn().getTag(),
+ r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId());
+
+ waitForIdle();
+ StatusBarNotification[] notifs =
+ mBinderService.getActiveNotifications(r.getSbn().getPackageName());
+ assertEquals(1, notifs.length);
+ }
+
+ @Test
+ public void testImmutableContextualActionIntent() throws Exception {
+ when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
+ .thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT);
+ when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true);
+ NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+ ArrayList<Notification.Action> extraAction = new ArrayList<>();
+ extraAction.add(new Notification.Action(0, "hello", null));
+ Bundle signals = new Bundle();
+ signals.putParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS, extraAction);
+ Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, "",
+ r.getUser());
+ r.addAdjustment(adjustment);
+ r.applyAdjustments();
+
+ mService.checkDisqualifyingFeatures(r.getUserId(), r.getUid(), r.getSbn().getId(),
+ r.getSbn().getTag(), r,false);
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
index 3281c3f..a80f62a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
@@ -32,6 +32,7 @@
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.app.IActivityManager;
import android.app.IUriGrantsManager;
@@ -154,7 +155,8 @@
mock(DevicePolicyManagerInternal.class), mock(IUriGrantsManager.class),
mock(UriGrantsManagerInternal.class),
mock(AppOpsManager.class), mUm, mock(NotificationHistoryManager.class),
- mock(StatsManager.class), mock(TelephonyManager.class));
+ mock(StatsManager.class), mock(TelephonyManager.class),
+ mock(ActivityManagerInternal.class));
} catch (SecurityException e) {
if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
throw e;