diff options
5 files changed, 690 insertions, 41 deletions
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index ec10913360af..0ddda12b0e2f 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -17,6 +17,7 @@ package android.app; import static android.Manifest.permission.POST_NOTIFICATIONS; +import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.service.notification.Flags.notificationClassification; @@ -50,6 +51,7 @@ import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.os.IpcDataCache; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; @@ -71,6 +73,8 @@ import android.util.LruCache; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.InstantSource; @@ -1202,12 +1206,20 @@ public class NotificationManager { * package (see {@link Context#createPackageContext(String, int)}).</p> */ public NotificationChannel getNotificationChannel(String channelId) { - INotificationManager service = service(); - try { - return service.getNotificationChannel(mContext.getOpPackageName(), - mContext.getUserId(), mContext.getPackageName(), channelId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return getChannelFromList(channelId, + mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId()))); + } else { + INotificationManager service = service(); + try { + return service.getNotificationChannel(mContext.getOpPackageName(), + mContext.getUserId(), mContext.getPackageName(), channelId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @@ -1222,13 +1234,21 @@ public class NotificationManager { */ public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId, @NonNull String conversationId) { - INotificationManager service = service(); - try { - return service.getConversationNotificationChannel(mContext.getOpPackageName(), - mContext.getUserId(), mContext.getPackageName(), channelId, true, - conversationId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return getConversationChannelFromList(channelId, conversationId, + mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId()))); + } else { + INotificationManager service = service(); + try { + return service.getConversationNotificationChannel(mContext.getOpPackageName(), + mContext.getUserId(), mContext.getPackageName(), channelId, true, + conversationId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @@ -1241,15 +1261,62 @@ public class NotificationManager { * {@link Context#createPackageContext(String, int)}).</p> */ public List<NotificationChannel> getNotificationChannels() { - INotificationManager service = service(); - try { - return service.getNotificationChannels(mContext.getOpPackageName(), - mContext.getPackageName(), mContext.getUserId()).getList(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId())); + } else { + INotificationManager service = service(); + try { + return service.getNotificationChannels(mContext.getOpPackageName(), + mContext.getPackageName(), mContext.getUserId()).getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } + // channel list assumed to be associated with the appropriate package & user id already. + private static NotificationChannel getChannelFromList(String channelId, + List<NotificationChannel> channels) { + if (channels == null) { + return null; + } + if (channelId == null) { + channelId = DEFAULT_CHANNEL_ID; + } + for (NotificationChannel channel : channels) { + if (channelId.equals(channel.getId())) { + return channel; + } + } + return null; + } + + private static NotificationChannel getConversationChannelFromList(String channelId, + String conversationId, List<NotificationChannel> channels) { + if (channels == null) { + return null; + } + if (channelId == null) { + channelId = DEFAULT_CHANNEL_ID; + } + if (conversationId == null) { + return getChannelFromList(channelId, channels); + } + NotificationChannel parent = null; + for (NotificationChannel channel : channels) { + if (conversationId.equals(channel.getConversationId()) + && channelId.equals(channel.getParentChannelId())) { + return channel; + } else if (channelId.equals(channel.getId())) { + parent = channel; + } + } + return parent; + } + /** * Deletes the given notification channel. * @@ -1328,6 +1395,71 @@ public class NotificationManager { } } + private static final String NOTIFICATION_CHANNEL_CACHE_API = "getNotificationChannel"; + private static final String NOTIFICATION_CHANNEL_LIST_CACHE_NAME = "getNotificationChannels"; + private static final int NOTIFICATION_CHANNEL_CACHE_SIZE = 10; + + private final IpcDataCache.QueryHandler<NotificationChannelQuery, List<NotificationChannel>> + mNotificationChannelListQueryHandler = new IpcDataCache.QueryHandler<>() { + @Override + public List<NotificationChannel> apply(NotificationChannelQuery query) { + INotificationManager service = service(); + try { + return service.getNotificationChannels(query.callingPkg, + query.targetPkg, query.userId).getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public boolean shouldBypassCache(@NonNull NotificationChannelQuery query) { + // Other locations should also not be querying the cache in the first place if + // the flag is not enabled, but this is an extra precaution. + if (!Flags.nmBinderPerfCacheChannels()) { + Log.wtf(TAG, + "shouldBypassCache called when nm_binder_perf_cache_channels off"); + return true; + } + return false; + } + }; + + private final IpcDataCache<NotificationChannelQuery, List<NotificationChannel>> + mNotificationChannelListCache = + new IpcDataCache<>(NOTIFICATION_CHANNEL_CACHE_SIZE, IpcDataCache.MODULE_SYSTEM, + NOTIFICATION_CHANNEL_CACHE_API, NOTIFICATION_CHANNEL_LIST_CACHE_NAME, + mNotificationChannelListQueryHandler); + + private record NotificationChannelQuery( + String callingPkg, + String targetPkg, + int userId) {} + + /** + * @hide + */ + public static void invalidateNotificationChannelCache() { + if (Flags.nmBinderPerfCacheChannels()) { + IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, + NOTIFICATION_CHANNEL_CACHE_API); + } else { + // if we are here, we have failed to flag something + Log.wtf(TAG, "invalidateNotificationChannelCache called without flag"); + } + } + + /** + * For testing only: running tests with a cache requires marking the cache's property for + * testing, as test APIs otherwise cannot invalidate the cache. This must be called after + * calling PropertyInvalidatedCache.setTestMode(true). + * @hide + */ + @VisibleForTesting + public void setChannelCacheToTestMode() { + mNotificationChannelListCache.testPropertyName(); + } + /** * @hide */ diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java index 6538ce85457c..84bcc39b8cc6 100644 --- a/core/tests/coretests/src/android/app/NotificationManagerTest.java +++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java @@ -16,6 +16,8 @@ package android.app; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -25,16 +27,21 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.UserHandle; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; +import android.testing.TestableContext; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -42,6 +49,7 @@ import org.junit.runner.RunWith; import java.time.Instant; import java.time.InstantSource; +import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest @@ -50,14 +58,25 @@ public class NotificationManagerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private Context mContext; private NotificationManagerWithMockService mNotificationManager; private final FakeClock mClock = new FakeClock(); + @Rule + public final PackageTestableContext mContext = new PackageTestableContext( + ApplicationProvider.getApplicationContext()); + @Before public void setUp() { - mContext = ApplicationProvider.getApplicationContext(); mNotificationManager = new NotificationManagerWithMockService(mContext, mClock); + + // Caches must be in test mode in order to be used in tests. + PropertyInvalidatedCache.setTestMode(true); + mNotificationManager.setChannelCacheToTestMode(); + } + + @After + public void tearDown() { + PropertyInvalidatedCache.setTestMode(false); } @Test @@ -243,12 +262,161 @@ public class NotificationManagerTest { anyInt(), any(), anyInt()); } + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_cachedUntilInvalidated() throws Exception { + // Invalidate the cache first because the cache won't do anything until then + NotificationManager.invalidateNotificationChannelCache(); + + // It doesn't matter what the returned contents are, as long as we return a channel. + // This setup must set up getNotificationChannels(), as that's the method called. + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel()))); + + // ask for the same channel 100 times without invalidating the cache + for (int i = 0; i < 100; i++) { + NotificationChannel unused = mNotificationManager.getNotificationChannel("id"); + } + + // invalidate the cache; then ask again + NotificationManager.invalidateNotificationChannelCache(); + NotificationChannel unused = mNotificationManager.getNotificationChannel("id"); + + verify(mNotificationManager.mBackendService, times(2)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_sameApp_oneCall() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + + NotificationChannel c1 = new NotificationChannel("id1", "name1", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel c2 = new NotificationChannel("id2", "name2", + NotificationManager.IMPORTANCE_NONE); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(c1, c2))); + + assertThat(mNotificationManager.getNotificationChannel("id1")).isEqualTo(c1); + assertThat(mNotificationManager.getNotificationChannel("id2")).isEqualTo(c2); + assertThat(mNotificationManager.getNotificationChannel("id3")).isNull(); + + verify(mNotificationManager.mBackendService, times(1)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannels_cachedUntilInvalidated() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel()))); + + // ask for channels 100 times without invalidating the cache + for (int i = 0; i < 100; i++) { + List<NotificationChannel> unused = mNotificationManager.getNotificationChannels(); + } + + // invalidate the cache; then ask again + NotificationManager.invalidateNotificationChannelCache(); + List<NotificationChannel> res = mNotificationManager.getNotificationChannels(); + + verify(mNotificationManager.mBackendService, times(2)) + .getNotificationChannels(any(), any(), anyInt()); + assertThat(res).containsExactlyElementsIn(List.of(exampleChannel())); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_channelAndConversationLookup() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + + // Full list of channels: c1; conv1 = child of c1; c2 is unrelated + NotificationChannel c1 = new NotificationChannel("id", "name", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel conv1 = new NotificationChannel("", "name_conversation", + NotificationManager.IMPORTANCE_DEFAULT); + conv1.setConversationId("id", "id_conversation"); + NotificationChannel c2 = new NotificationChannel("other", "name2", + NotificationManager.IMPORTANCE_DEFAULT); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), anyInt())) + .thenReturn(new ParceledListSlice<>(List.of(c1, conv1, c2))); + + // Lookup for channel c1 and c2: returned as expected + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(c1); + assertThat(mNotificationManager.getNotificationChannel("other")).isEqualTo(c2); + + // Lookup for conv1 should return conv1 + assertThat(mNotificationManager.getNotificationChannel("id", "id_conversation")).isEqualTo( + conv1); + + // Lookup for a different conversation channel that doesn't exist, whose parent channel id + // is "id", should return c1 + assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isEqualTo(c1); + + // Lookup of a nonexistent channel is null + assertThat(mNotificationManager.getNotificationChannel("id3")).isNull(); + + // All of that should have been one call to getNotificationChannels() + verify(mNotificationManager.mBackendService, times(1)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_differentPackages() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + final String pkg1 = "one"; + final String pkg2 = "two"; + final int userId = 0; + final int userId1 = 1; + + // multiple channels with the same ID, but belonging to different packages/users + NotificationChannel channel1 = new NotificationChannel("id", "name1", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel channel2 = channel1.copy(); + channel2.setName("name2"); + NotificationChannel channel3 = channel1.copy(); + channel3.setName("name3"); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1), + eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel1))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg2), + eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel2))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1), + eq(userId1))).thenReturn(new ParceledListSlice<>(List.of(channel3))); + + // set our context to pretend to be from package 1 and userId 0 + mContext.setParameters(pkg1, pkg1, userId); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel1); + + // now package 2 + mContext.setParameters(pkg2, pkg2, userId); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel2); + + // now pkg1 for a different user + mContext.setParameters(pkg1, pkg1, userId1); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel3); + + // Those should have been three different calls + verify(mNotificationManager.mBackendService, times(3)) + .getNotificationChannels(any(), any(), anyInt()); + } + private Notification exampleNotification() { return new Notification.Builder(mContext, "channel") .setSmallIcon(android.R.drawable.star_big_on) .build(); } + private NotificationChannel exampleChannel() { + return new NotificationChannel("id", "channel_name", + NotificationManager.IMPORTANCE_DEFAULT); + } + private static class NotificationManagerWithMockService extends NotificationManager { private final INotificationManager mBackendService; @@ -264,6 +432,48 @@ public class NotificationManagerTest { } } + // Helper TestableContext class where we can control just the return values of getPackageName, + // getOpPackageName, and getUserId (used in getNotificationChannels). + private static class PackageTestableContext extends TestableContext { + private String mPackage; + private String mOpPackage; + private Integer mUserId; + + PackageTestableContext(Context base) { + super(base); + } + + void setParameters(String packageName, String opPackageName, int userId) { + mPackage = packageName; + mOpPackage = opPackageName; + mUserId = userId; + } + + @Override + public String getPackageName() { + if (mPackage != null) return mPackage; + return super.getPackageName(); + } + + @Override + public String getOpPackageName() { + if (mOpPackage != null) return mOpPackage; + return super.getOpPackageName(); + } + + @Override + public int getUserId() { + if (mUserId != null) return mUserId; + return super.getUserId(); + } + + @Override + public UserHandle getUser() { + if (mUserId != null) return UserHandle.of(mUserId); + return super.getUser(); + } + } + private static class FakeClock implements InstantSource { private long mNowMillis = 441644400000L; diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 7375a68c547b..20b83b268990 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -3136,6 +3136,7 @@ public class NotificationManagerService extends SystemService { mAssistants.onBootPhaseAppsCanStart(); mConditionProviders.onBootPhaseAppsCanStart(); mHistoryManager.onBootPhaseAppsCanStart(); + mPreferencesHelper.onBootPhaseAppsCanStart(); migrateDefaultNAS(); maybeShowInitialReviewPermissionsNotification(); diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 15377d6b269a..9d25d18df87c 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -82,7 +82,6 @@ import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntArray; -import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseBooleanArray; @@ -272,6 +271,15 @@ public class PreferencesHelper implements RankingConfig { updateMediaNotificationFilteringEnabled(); } + void onBootPhaseAppsCanStart() { + // IpcDataCaches must be invalidated once data becomes available, as queries will only + // begin to be cached after the first invalidation signal. At this point, we know about all + // notification channels. + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } + } + public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId) throws XmlPullParserException, IOException { int type = parser.getEventType(); @@ -531,12 +539,14 @@ public class PreferencesHelper implements RankingConfig { private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg, @UserIdInt int userId, int uid, int importance, int priority, int visibility, boolean showBadge, int bubblePreference, long creationTime) { + boolean created = false; final String key = packagePreferencesKey(pkg, uid); PackagePreferences r = (uid == UNKNOWN_UID) ? mRestoredWithoutUids.get(unrestoredPackageKey(pkg, userId)) : mPackagePreferences.get(key); if (r == null) { + created = true; r = new PackagePreferences(); r.pkg = pkg; r.uid = uid; @@ -572,6 +582,9 @@ public class PreferencesHelper implements RankingConfig { mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, userId)); } } + if (android.app.Flags.nmBinderPerfCacheChannels() && created) { + invalidateNotificationChannelCache(); + } return r; } @@ -664,6 +677,9 @@ public class PreferencesHelper implements RankingConfig { } NotificationChannel channel = new NotificationChannel(channelId, label, IMPORTANCE_LOW); p.channels.put(channelId, channel); + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } return channel; } @@ -1208,6 +1224,10 @@ public class PreferencesHelper implements RankingConfig { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + if (android.app.Flags.nmBinderPerfCacheChannels() && needsPolicyFileChange) { + invalidateNotificationChannelCache(); + } + return needsPolicyFileChange; } @@ -1229,6 +1249,9 @@ public class PreferencesHelper implements RankingConfig { } channel.unlockFields(USER_LOCKED_IMPORTANCE); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } @@ -1301,6 +1324,9 @@ public class PreferencesHelper implements RankingConfig { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } if (changed) { + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } updateConfig(); } } @@ -1537,6 +1563,10 @@ public class PreferencesHelper implements RankingConfig { if (channelBypassedDnd) { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + + if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannel) { + invalidateNotificationChannelCache(); + } return deletedChannel; } @@ -1566,6 +1596,9 @@ public class PreferencesHelper implements RankingConfig { } r.channels.remove(channelId); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } @Override @@ -1576,13 +1609,18 @@ public class PreferencesHelper implements RankingConfig { if (r == null) { return; } + boolean deleted = false; int N = r.channels.size() - 1; for (int i = N; i >= 0; i--) { String key = r.channels.keyAt(i); if (!DEFAULT_CHANNEL_ID.equals(key)) { r.channels.remove(key); + deleted = true; } } + if (android.app.Flags.nmBinderPerfCacheChannels() && deleted) { + invalidateNotificationChannelCache(); + } } } @@ -1613,6 +1651,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public void updateDefaultApps(int userId, ArraySet<String> toRemove, @@ -1642,6 +1683,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg, @@ -1757,6 +1801,9 @@ public class PreferencesHelper implements RankingConfig { if (groupBypassedDnd) { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannels.size() > 0) { + invalidateNotificationChannelCache(); + } return deletedChannels; } @@ -1902,8 +1949,13 @@ public class PreferencesHelper implements RankingConfig { } } } - if (!deletedChannelIds.isEmpty() && mCurrentUserHasChannelsBypassingDnd) { - updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + if (!deletedChannelIds.isEmpty()) { + if (mCurrentUserHasChannelsBypassingDnd) { + updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } return deletedChannelIds; } @@ -2196,6 +2248,11 @@ public class PreferencesHelper implements RankingConfig { PackagePreferences prefs = getOrCreatePackagePreferencesLocked(sourcePkg, sourceUid); prefs.delegate = new Delegate(delegatePkg, delegateUid, true); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + // If package delegates change, then which packages can get what channel information + // also changes, so we need to clear the cache. + invalidateNotificationChannelCache(); + } } /** @@ -2208,6 +2265,9 @@ public class PreferencesHelper implements RankingConfig { prefs.delegate.mEnabled = false; } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } /** @@ -2811,18 +2871,24 @@ public class PreferencesHelper implements RankingConfig { public void onUserRemoved(int userId) { synchronized (mLock) { + boolean removed = false; int N = mPackagePreferences.size(); for (int i = N - 1; i >= 0; i--) { PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i); if (UserHandle.getUserId(PackagePreferences.uid) == userId) { mPackagePreferences.removeAt(i); + removed = true; } } + if (android.app.Flags.nmBinderPerfCacheChannels() && removed) { + invalidateNotificationChannelCache(); + } } } protected void onLocaleChanged(Context context, int userId) { synchronized (mLock) { + boolean updated = false; int N = mPackagePreferences.size(); for (int i = 0; i < N; i++) { PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i); @@ -2833,10 +2899,14 @@ public class PreferencesHelper implements RankingConfig { DEFAULT_CHANNEL_ID).setName( context.getResources().getString( R.string.default_notification_channel_label)); + updated = true; } // TODO (b/346396459): Localize all reserved channels } } + if (android.app.Flags.nmBinderPerfCacheChannels() && updated) { + invalidateNotificationChannelCache(); + } } } @@ -2884,7 +2954,7 @@ public class PreferencesHelper implements RankingConfig { channel.getAudioAttributes().getUsage()); if (Settings.System.DEFAULT_NOTIFICATION_URI.equals( restoredUri)) { - Log.w(TAG, + Slog.w(TAG, "Could not restore sound: " + uri + " for channel: " + channel); } @@ -2922,6 +2992,9 @@ public class PreferencesHelper implements RankingConfig { if (updated) { updateConfig(); + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } return updated; } @@ -2939,6 +3012,9 @@ public class PreferencesHelper implements RankingConfig { p.priority = DEFAULT_PRIORITY; p.visibility = DEFAULT_VISIBILITY; p.showBadge = DEFAULT_SHOW_BADGE; + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } } } @@ -3123,6 +3199,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public void migrateNotificationPermissions(List<UserInfo> users) { @@ -3154,6 +3233,12 @@ public class PreferencesHelper implements RankingConfig { mRankingHandler.requestSort(); } + @VisibleForTesting + // Utility method for overriding in tests to confirm that the cache gets cleared. + protected void invalidateNotificationChannelCache() { + NotificationManager.invalidateNotificationChannelCache(); + } + private static String packagePreferencesKey(String pkg, int uid) { return pkg + "|" + uid; } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 8e79514c875e..8cc233b7594b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -164,6 +164,7 @@ import com.android.os.AtomsProto.PackageNotificationChannelPreferences; import com.android.os.AtomsProto.PackageNotificationPreferences; import com.android.server.UiServiceTestCase; import com.android.server.notification.PermissionHelper.PackagePermission; +import com.android.server.uri.UriGrantsManagerInternal; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -179,6 +180,9 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -199,9 +203,6 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadLocalRandom; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @EnableFlags(FLAG_PERSIST_INCOMPLETE_RESTORE_DATA) @@ -239,9 +240,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { private NotificationManager.Policy mTestNotificationPolicy; - private PreferencesHelper mHelper; - // fresh object for testing xml reading - private PreferencesHelper mXmlHelper; + private TestPreferencesHelper mHelper; + // fresh object for testing xml reading; also TestPreferenceHelper in order to avoid interacting + // with real IpcDataCaches + private TestPreferencesHelper mXmlHelper; private AudioAttributes mAudioAttributes; private NotificationChannelLoggerFake mLogger = new NotificationChannelLoggerFake(); @@ -378,10 +380,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(mUserProfiles.getCurrentProfileIds()).thenReturn(currentProfileIds); when(mClock.millis()).thenReturn(System.currentTimeMillis()); - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); resetZenModeHelper(); @@ -793,7 +795,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_oldXml_migrates() throws Exception { - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -929,7 +931,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception { - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -988,7 +990,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_permissionNotificationOff() throws Exception { - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock); @@ -1047,7 +1049,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception { - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -1641,7 +1643,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { serializer.flush(); // simulate load after reboot - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); loadByteArrayXml(baos.toByteArray(), false, USER_ALL); @@ -1696,7 +1698,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { Duration.ofDays(2).toMillis() + System.currentTimeMillis()); // simulate load after reboot - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); loadByteArrayXml(xml.getBytes(), false, USER_ALL); @@ -1774,10 +1776,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(contentResolver.getResourceId(ANDROID_RES_SOUND_URI)).thenReturn(resId).thenThrow( new FileNotFoundException("")).thenReturn(resId); - mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); - mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); @@ -6573,4 +6575,223 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.setCanBePromoted(PKG_P, UID_P, false, false); assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue(); } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_invalidateOnCreationAndChange() { + mHelper.resetCacheInvalidation(); + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // new channel should invalidate the cache. + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // when the channel data is updated, should invalidate the cache again after that. + mHelper.resetCacheInvalidation(); + NotificationChannel newChannel = channel.copy(); + newChannel.setName("new name"); + newChannel.setImportance(IMPORTANCE_HIGH); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // also for conversations + mHelper.resetCacheInvalidation(); + String parentId = "id"; + String convId = "conversation"; + NotificationChannel conv = new NotificationChannel( + String.format(CONVERSATION_CHANNEL_ID_FORMAT, parentId, convId), "conversation", + IMPORTANCE_DEFAULT); + conv.setConversationId(parentId, convId); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, conv, true, false, UID_N_MR1, + false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + mHelper.resetCacheInvalidation(); + NotificationChannel newConv = conv.copy(); + newConv.setName("changed"); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newConv, true, UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_invalidateOnDelete() { + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // ignore any invalidations up until now + mHelper.resetCacheInvalidation(); + + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // recreate channel and now permanently delete + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + mHelper.resetCacheInvalidation(); + mHelper.permanentlyDeleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id"); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_noInvalidationWhenNoChange() { + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // ignore any invalidations up until now + mHelper.resetCacheInvalidation(); + + // newChannel, same as the old channel + NotificationChannel newChannel = channel.copy(); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, false, UID_N_MR1, + false); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false); + + // because there were no effective changes, we should not see any cache invalidations + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // deletions of a nonexistent channel also don't change anything + mHelper.resetCacheInvalidation(); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "nonexistent", UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_multipleUsersAndPackages() { + // Setup: create channels for: + // pkg O, user + // pkg O, work (same channel ID, different user) + // pkg N_MR1, user + // pkg N_MR1, user, conversation child of above + String p2u1ConvId = String.format(CONVERSATION_CHANNEL_ID_FORMAT, "p2", "conv"); + NotificationChannel p1u1 = new NotificationChannel("p1", "p1u1", IMPORTANCE_DEFAULT); + NotificationChannel p1u2 = new NotificationChannel("p1", "p1u2", IMPORTANCE_DEFAULT); + NotificationChannel p2u1 = new NotificationChannel("p2", "p2u1", IMPORTANCE_DEFAULT); + NotificationChannel p2u1Conv = new NotificationChannel(p2u1ConvId, "p2u1 conv", + IMPORTANCE_DEFAULT); + p2u1Conv.setConversationId("p2", "conv"); + + mHelper.createNotificationChannel(PKG_O, UID_O, p1u1, true, + false, UID_O, false); + mHelper.createNotificationChannel(PKG_O, UID_O + UserHandle.PER_USER_RANGE, p1u2, true, + false, UID_O + UserHandle.PER_USER_RANGE, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1, true, + false, UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1Conv, true, + false, UID_N_MR1, false); + mHelper.resetCacheInvalidation(); + + // Update to an existent channel, with a change: should invalidate + NotificationChannel p1u1New = p1u1.copy(); + p1u1New.setName("p1u1 new"); + mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New, true, UID_O, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // Do it again, but no change for this user + mHelper.resetCacheInvalidation(); + mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New.copy(), true, UID_O, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // Delete conversations, but for a package without those conversations + mHelper.resetCacheInvalidation(); + mHelper.deleteConversations(PKG_O, UID_O, Set.of(p2u1Conv.getConversationId()), UID_O, + false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // Now delete conversations for the right package + mHelper.resetCacheInvalidation(); + mHelper.deleteConversations(PKG_N_MR1, UID_N_MR1, Set.of(p2u1Conv.getConversationId()), + UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_userRemoved() throws Exception { + NotificationChannel c1 = new NotificationChannel("id1", "name1", IMPORTANCE_DEFAULT); + int uid1 = UserHandle.getUid(1, 1); + setUpPackageWithUid("pkg1", uid1); + mHelper.createNotificationChannel("pkg1", uid1, c1, true, false, uid1, false); + mHelper.resetCacheInvalidation(); + + // delete user 1; should invalidate cache + mHelper.onUserRemoved(1); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_packagesChanged() { + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + + // package deleted: expect cache invalidation + mHelper.resetCacheInvalidation(); + mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1}, + new int[]{UID_N_MR1}); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // re-created: expect cache invalidation again + mHelper.resetCacheInvalidation(); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + mHelper.onPackagesChanged(false, USER_SYSTEM, new String[]{PKG_N_MR1}, + new int[]{UID_N_MR1}); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @DisableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_flagOff_neverTouchesCache() { + // Do a bunch of channel-changing operations. + NotificationChannel channel = + new NotificationChannel("id", "name1", NotificationManager.IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); + + NotificationChannel copy = channel.copy(); + copy.setName("name2"); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, copy, true, UID_N_MR1, false); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false); + + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + } + + // Test version of PreferencesHelper whose only functional difference is that it does not + // interact with the real IpcDataCache, and instead tracks whether or not the cache has been + // invalidated since creation or the last reset. + private static class TestPreferencesHelper extends PreferencesHelper { + private boolean mCacheInvalidated = false; + + TestPreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler, + ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager, + NotificationChannelLogger notificationChannelLogger, + AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles, + UriGrantsManagerInternal ugmInternal, + boolean showReviewPermissionsNotification, Clock clock) { + super(context, pm, rankingHandler, zenHelper, permHelper, permManager, + notificationChannelLogger, appOpsManager, userProfiles, ugmInternal, + showReviewPermissionsNotification, clock); + } + + @Override + protected void invalidateNotificationChannelCache() { + mCacheInvalidated = true; + } + + boolean hasCacheBeenInvalidated() { + return mCacheInvalidated; + } + + void resetCacheInvalidation() { + mCacheInvalidated = false; + } + } } |