From b320763eb82eb81e42df5b649658b78742ad0b5e Mon Sep 17 00:00:00 2001 From: Christopher Tate Date: Wed, 18 Nov 2020 17:22:45 -0800 Subject: [PATCH] DO NOT MERGE - Disallow deletion of channels with FGS notifications Bug: 156090809 Test: atest CtsAppTestCases:NotificationManagerTest Test: atest CtsAppTestCases:android.app.cts.ServiceTest Change-Id: I1c2bb78d86f194585d273661cecf3419f51965df Merged-In: Ie917a4d1b4595a576c4ea5ea079ecc773000abdc --- .../android/app/ActivityManagerInternal.java | 15 +++++++ .../com/android/server/am/ActiveServices.java | 40 +++++++++++++++++++ .../server/am/ActivityManagerService.java | 17 ++++++++ .../NotificationManagerService.java | 36 +++++++++++++++-- .../NotificationManagerServiceTest.java | 5 ++- 5 files changed, 108 insertions(+), 5 deletions(-) diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 9dceb7f9e4339..768393c0c7630 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -273,4 +273,19 @@ public abstract class ActivityManagerInternal { * Returns {@code true} if {@code uid} is running an activity from {@code packageName}. */ public abstract boolean hasRunningActivity(int uid, @Nullable String packageName); + + /** + * Returns {@code true} if the given notification channel currently has a + * notification associated with a foreground service. This is an AMS check + * because that is the source of truth for the FGS state. + */ + public abstract boolean hasForegroundServiceNotification(String pkg, int userId, + String channelId); + + /** + * If the given app has any FGSs whose notifications are in the given channel, + * stop them. + */ + public abstract void stopForegroundServicesForChannel(String pkg, int userId, + String channelId); } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 8e0bf6fada37d..67443611cf78f 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Set; import android.app.ActivityThread; @@ -311,6 +312,45 @@ public final class ActiveServices { return smap != null ? smap.mStartingBackground.size() >= mMaxStartingBackground : false; } + boolean hasForegroundServiceNotificationLocked(String pkg, int userId, String channelId) { + final ServiceMap smap = mServiceMap.get(userId); + if (smap != null) { + for (int i = 0; i < smap.mServicesByName.size(); i++) { + final ServiceRecord sr = smap.mServicesByName.valueAt(i); + if (sr.appInfo.packageName.equals(pkg) && sr.isForeground) { + if (Objects.equals(sr.foregroundNoti.getChannelId(), channelId)) { + if (DEBUG_FOREGROUND_SERVICE) { + Slog.d(TAG_SERVICE, "Channel u" + userId + "/pkg=" + pkg + + "/channelId=" + channelId + + " has fg service notification"); + } + return true; + } + } + } + } + return false; + } + + void stopForegroundServicesForChannelLocked(String pkg, int userId, String channelId) { + final ServiceMap smap = mServiceMap.get(userId); + if (smap != null) { + for (int i = 0; i < smap.mServicesByName.size(); i++) { + final ServiceRecord sr = smap.mServicesByName.valueAt(i); + if (sr.appInfo.packageName.equals(pkg) && sr.isForeground) { + if (Objects.equals(sr.foregroundNoti.getChannelId(), channelId)) { + if (DEBUG_FOREGROUND_SERVICE) { + Slog.d(TAG_SERVICE, "Stopping FGS u" + userId + "/pkg=" + pkg + + "/channelId=" + channelId + + " for conversation channel clear"); + } + stopServiceLocked(sr); + } + } + } + } + } + private ServiceMap getServiceMapLocked(int callingUser) { ServiceMap smap = mServiceMap.get(callingUser); if (smap == null) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 06825d36c2540..bedcc3ba3ce9e 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -24305,6 +24305,23 @@ public class ActivityManagerService extends IActivityManager.Stub } return false; } + + @Override + public boolean hasForegroundServiceNotification(String pkg, int userId, + String channelId) { + synchronized (ActivityManagerService.this) { + return mServices.hasForegroundServiceNotificationLocked(pkg, userId, channelId); + } + } + + @Override + public void stopForegroundServicesForChannel(String pkg, int userId, + String channelId) { + synchronized (ActivityManagerService.this) { + mServices.stopForegroundServicesForChannelLocked(pkg, userId, channelId); + } + } + } /** diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 797ce37af5609..39cb2e079db2e 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -272,6 +272,7 @@ public class NotificationManagerService extends SystemService { private IActivityManager mAm; private ActivityManager mActivityManager; + private ActivityManagerInternal mAmi; private IPackageManager mPackageManager; private PackageManager mPackageManagerClient; AudioManager mAudioManager; @@ -1209,7 +1210,8 @@ public class NotificationManagerService extends SystemService { NotificationAssistants notificationAssistants, ConditionProviders conditionProviders, ICompanionDeviceManager companionManager, SnoozeHelper snoozeHelper, NotificationUsageStats usageStats, AtomicFile policyFile, - ActivityManager activityManager, GroupHelper groupHelper) { + ActivityManager activityManager, GroupHelper groupHelper, + ActivityManagerInternal ami) { Resources resources = getContext().getResources(); mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE, @@ -1226,6 +1228,7 @@ public class NotificationManagerService extends SystemService { mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); mCompanionManager = companionManager; mActivityManager = activityManager; + mAmi = ami; mHandler = new WorkerHandler(looper); mRankingThread.start(); @@ -1353,7 +1356,8 @@ public class NotificationManagerService extends SystemService { null, snoozeHelper, new NotificationUsageStats(getContext()), new AtomicFile(new File(systemDir, "notification_policy.xml")), (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE), - getGroupHelper()); + getGroupHelper(), + LocalServices.getService(ActivityManagerInternal.class)); // register for various Intents IntentFilter filter = new IntentFilter(); @@ -1863,15 +1867,32 @@ public class NotificationManagerService extends SystemService { return mRankingHelper.getNotificationChannel(pkg, uid, channelId, includeDeleted); } + // Returns 'true' if the given channel has a notification associated + // with an active foreground service. + private void enforceDeletingChannelHasNoFgService(String pkg, int userId, + String channelId) { + if (mAmi.hasForegroundServiceNotification(pkg, userId, channelId)) { + // Would be a behavioral change to introduce a throw here, so + // we simply return without affecting the channel. + Slog.w(TAG, "Package u" + userId + "/" + pkg + + " may not delete notification channel '" + + channelId + "' with fg service"); + throw new SecurityException("Not allowed to delete channel " + channelId + + " with a foreground service"); + } + } + @Override public void deleteNotificationChannel(String pkg, String channelId) { checkCallerIsSystemOrSameApp(pkg); final int callingUid = Binder.getCallingUid(); + final int callingUser = UserHandle.getUserId(callingUid); if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channelId)) { throw new IllegalArgumentException("Cannot delete default channel"); } + enforceDeletingChannelHasNoFgService(pkg, callingUser, channelId); cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, true, - UserHandle.getUserId(callingUid), REASON_CHANNEL_BANNED, null); + callingUser, REASON_CHANNEL_BANNED, null); mRankingHelper.deleteNotificationChannel(pkg, callingUid, channelId); mListeners.notifyNotificationChannelChanged(pkg, UserHandle.getUserHandleForUid(callingUid), @@ -1896,13 +1917,20 @@ public class NotificationManagerService extends SystemService { NotificationChannelGroup groupToDelete = mRankingHelper.getNotificationChannelGroup(groupId, pkg, callingUid); if (groupToDelete != null) { + // Preflight for allowability + final int userId = UserHandle.getUserId(callingUid); + List groupChannels = groupToDelete.getChannels(); + for (int i = 0; i < groupChannels.size(); i++) { + enforceDeletingChannelHasNoFgService(pkg, userId, + groupChannels.get(i).getId()); + } List deletedChannels = mRankingHelper.deleteNotificationChannelGroup(pkg, callingUid, groupId); for (int i = 0; i < deletedChannels.size(); i++) { final NotificationChannel deletedChannel = deletedChannels.get(i); cancelAllNotificationsInt(MY_UID, MY_PID, pkg, deletedChannel.getId(), 0, 0, true, - UserHandle.getUserId(Binder.getCallingUid()), REASON_CHANNEL_BANNED, + userId, REASON_CHANNEL_BANNED, null); mListeners.notifyNotificationChannelChanged(pkg, UserHandle.getUserHandleForUid(callingUid), diff --git a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java index 51de2c21e3a80..1e788c9ab7df0 100644 --- a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -45,6 +45,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.ActivityManagerInternal; import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; @@ -124,6 +125,8 @@ public class NotificationManagerServiceTest extends NotificationTestCase { private AudioManager mAudioManager; @Mock ActivityManager mActivityManager; + @Mock + ActivityManagerInternal mAmi; NotificationManagerService.WorkerHandler mHandler; private NotificationChannel mTestNotificationChannel = new NotificationChannel( @@ -213,7 +216,7 @@ public class NotificationManagerServiceTest extends NotificationTestCase { mPackageManager, mPackageManagerClient, mockLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr, mSnoozeHelper, mUsageStats, mPolicyFile, mActivityManager, - mGroupHelper); + mGroupHelper, mAmi); } catch (SecurityException e) { if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) { throw e;