Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2022fc79 authored by Yuri Lin's avatar Yuri Lin Committed by Android (Google) Code Review
Browse files

Merge "Cache calls to getNotificationChannel*." into main

parents 0ec8a218 69da62bd
Loading
Loading
Loading
Loading
+151 −19
Original line number Diff line number Diff line
@@ -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,6 +1206,13 @@ public class NotificationManager {
     * package (see {@link Context#createPackageContext(String, int)}).</p>
     */
    public NotificationChannel getNotificationChannel(String channelId) {
        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(),
@@ -1210,6 +1221,7 @@ public class NotificationManager {
                throw e.rethrowFromSystemServer();
            }
        }
    }

    /**
     * Returns the notification channel settings for a given channel and
@@ -1222,6 +1234,13 @@ public class NotificationManager {
     */
    public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId,
            @NonNull String conversationId) {
        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(),
@@ -1231,6 +1250,7 @@ public class NotificationManager {
                throw e.rethrowFromSystemServer();
            }
        }
    }

    /**
     * Returns all notification channels belonging to the calling package.
@@ -1241,6 +1261,12 @@ public class NotificationManager {
     * {@link Context#createPackageContext(String, int)}).</p>
     */
    public List<NotificationChannel> getNotificationChannels() {
        if (Flags.nmBinderPerfCacheChannels()) {
            return mNotificationChannelListCache.query(new NotificationChannelQuery(
               mContext.getOpPackageName(),
               mContext.getPackageName(),
               mContext.getUserId()));
        } else {
            INotificationManager service = service();
            try {
                return service.getNotificationChannels(mContext.getOpPackageName(),
@@ -1249,6 +1275,47 @@ public class NotificationManager {
                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
     */
+212 −2
Original line number Diff line number Diff line
@@ -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;
+1 −0
Original line number Diff line number Diff line
@@ -3136,6 +3136,7 @@ public class NotificationManagerService extends SystemService {
            mAssistants.onBootPhaseAppsCanStart();
            mConditionProviders.onBootPhaseAppsCanStart();
            mHistoryManager.onBootPhaseAppsCanStart();
            mPreferencesHelper.onBootPhaseAppsCanStart();
            migrateDefaultNAS();
            maybeShowInitialReviewPermissionsNotification();
+89 −4

File changed.

Preview size limit exceeded, changes collapsed.

+237 −16

File changed.

Preview size limit exceeded, changes collapsed.