diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 269bd20e8f647..23798c0622fc5 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 @@ public class NotificationManagerService extends SystemService { private IActivityManager mAm; private ActivityTaskManagerInternal mAtm; private ActivityManager mActivityManager; + private ActivityManagerInternal mAmi; private IPackageManager mPackageManager; private PackageManager mPackageManagerClient; AudioManager mAudioManager; @@ -1897,7 +1898,7 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { + " 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 @@ public class NotificationManagerService extends SystemService { // 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 ed6a20b97d374..6ee5831642843 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_PEEK; 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.ContentUris; 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Resources mResources; @Mock RankingHandler mRankingHandler; + @Mock + ActivityManagerInternal mAmi; + + @Mock + IIntentSender pi1; private static final int MAX_POST_DELAY = 1000; @@ -392,7 +401,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 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 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 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 3281c3f4cfb95..a80f62ab09eef 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.verify; 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 @@ public class RoleObserverTest extends UiServiceTestCase { 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;