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

Commit 833868c7 authored by Ioana Alexandru's avatar Ioana Alexandru Committed by Android (Google) Code Review
Browse files

Merge changes from topic "notificon_dump" into main

* changes:
  Make AppIconProvider&NotifIconStyleProvider dumpable
  Implement caching in NotifIconStyleProvider
  Implement caching in AppIconProvider
parents 44bf6e08 b2181a83
Loading
Loading
Loading
Loading
+125 −3
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE;
import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -36,9 +37,11 @@ import static org.mockito.Mockito.when;

import static java.util.Objects.requireNonNull;

import android.app.Flags;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.RemoteException;
import android.platform.test.annotations.EnableFlags;
import android.testing.TestableLooper;

import androidx.annotation.NonNull;
@@ -61,6 +64,7 @@ import com.android.systemui.statusbar.notification.collection.inflation.NotifInf
import com.android.systemui.statusbar.notification.collection.inflation.NotifUiAdjustmentProvider;
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
@@ -69,6 +73,8 @@ import com.android.systemui.statusbar.notification.collection.provider.SectionSt
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn;
import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
import com.android.systemui.statusbar.notification.row.icon.AppIconProvider;
import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
import com.android.systemui.util.settings.SecureSettings;

@@ -82,10 +88,12 @@ import org.mockito.MockitoAnnotations;
import org.mockito.Spy;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -93,6 +101,7 @@ import java.util.Map;
public class PreparationCoordinatorTest extends SysuiTestCase {
    private NotifCollectionListener mCollectionListener;
    private OnBeforeFinalizeFilterListener mBeforeFilterListener;
    private OnBeforeTransformGroupsListener mBeforeTransformGroupsListener;
    private NotifFilter mUninflatedFilter;
    private NotifFilter mInflationErrorFilter;
    private NotifInflationErrorManager mErrorManager;
@@ -101,6 +110,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase {

    @Captor private ArgumentCaptor<NotifCollectionListener> mCollectionListenerCaptor;
    @Captor private ArgumentCaptor<OnBeforeFinalizeFilterListener> mBeforeFilterListenerCaptor;
    @Captor private ArgumentCaptor<OnBeforeTransformGroupsListener>
            mBeforeTransformGroupsListenerCaptor;
    @Captor private ArgumentCaptor<NotifInflater.Params> mParamsCaptor;

    @Mock private NotifSectioner mNotifSectioner;
@@ -108,13 +119,14 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
    @Mock private NotifPipeline mNotifPipeline;
    @Mock private IStatusBarService mService;
    @Mock private BindEventManagerImpl mBindEventManagerImpl;
    @Mock private AppIconProvider mAppIconProvider;
    @Mock private NotificationIconStyleProvider mNotificationIconStyleProvider;
    @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
    @Mock private SensitiveNotificationProtectionController mSensitiveNotifProtectionController;
    @Mock private Handler mHandler;
    @Mock private SecureSettings mSecureSettings;
    @Spy private FakeNotifInflater mNotifInflater = new FakeNotifInflater();
    @Mock
    HighPriorityProvider mHighPriorityProvider;
    @Mock HighPriorityProvider mHighPriorityProvider;
    private SectionStyleProvider mSectionStyleProvider;
    @Mock private UserTracker mUserTracker;
    @Mock private GroupMembershipManager mGroupMembershipManager;
@@ -126,6 +138,11 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
        return new NotificationEntryBuilder().setSection(mNotifSection);
    }

    @NonNull
    private GroupEntryBuilder getGroupEntryBuilder() {
        return new GroupEntryBuilder().setSection(mNotifSection);
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
@@ -153,6 +170,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
                mAdjustmentProvider,
                mService,
                mBindEventManagerImpl,
                mAppIconProvider,
                mNotificationIconStyleProvider,
                TEST_CHILD_BIND_CUTOFF,
                TEST_MAX_GROUP_DELAY);

@@ -163,6 +182,15 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
        mInflationErrorFilter = filters.get(0);
        mUninflatedFilter = filters.get(1);

        if (android.app.Flags.notificationsRedesignAppIcons()) {
            verify(mNotifPipeline).addOnBeforeTransformGroupsListener(
                    mBeforeTransformGroupsListenerCaptor.capture());
            mBeforeTransformGroupsListener = mBeforeTransformGroupsListenerCaptor.getValue();
        } else {
            verify(mNotifPipeline, never()).addOnBeforeTransformGroupsListener(
                    mBeforeTransformGroupsListenerCaptor.capture());
        }

        verify(mNotifPipeline).addCollectionListener(mCollectionListenerCaptor.capture());
        mCollectionListener = mCollectionListenerCaptor.getValue();

@@ -198,6 +226,100 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
        assertTrue(mInflationErrorFilter.shouldFilterOut(mEntry, 0));
    }

    @Test
    @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_APP_ICONS)
    public void testPurgesAppIconProviderCache() {
        // GIVEN a notification list
        NotificationEntry entry1 = getNotificationEntryBuilder().setPkg("1").build();
        NotificationEntry entry2 = getNotificationEntryBuilder().setPkg("2").build();
        NotificationEntry entry2bis = getNotificationEntryBuilder().setPkg("2").build();
        NotificationEntry entry3 = getNotificationEntryBuilder().setPkg("3").build();

        String groupKey1 = "group1";
        NotificationEntry summary =
                getNotificationEntryBuilder()
                        .setPkg(groupKey1)
                        .setGroup(mContext, groupKey1)
                        .setGroupSummary(mContext, true)
                        .build();
        NotificationEntry child1 = getNotificationEntryBuilder().setGroup(mContext, groupKey1)
                .setPkg(groupKey1).build();
        NotificationEntry child2 = getNotificationEntryBuilder().setGroup(mContext, groupKey1)
                .setPkg(groupKey1).build();
        GroupEntry groupWithSummaryAndChildren = getGroupEntryBuilder().setKey(groupKey1)
                .setSummary(summary).addChild(child1).addChild(child2).build();

        String groupKey2 = "group2";
        NotificationEntry summary2 =
                getNotificationEntryBuilder()
                        .setPkg(groupKey2)
                        .setGroup(mContext, groupKey2)
                        .setGroupSummary(mContext, true)
                        .build();
        GroupEntry summaryOnlyGroup = getGroupEntryBuilder().setKey(groupKey2)
                .setSummary(summary2).build();

        // WHEN onBeforeTransformGroup is called
        mBeforeTransformGroupsListener.onBeforeTransformGroups(
                List.of(entry1, entry2, entry2bis, entry3,
                        groupWithSummaryAndChildren, summaryOnlyGroup));

        // THEN purge should be called
        ArgumentCaptor<Collection<String>> argumentCaptor = ArgumentCaptor.forClass(List.class);
        verify(mAppIconProvider).purgeCache(argumentCaptor.capture());
        List<String> actualList = argumentCaptor.getValue().stream().sorted().toList();
        List<String> expectedList = Stream.of("1", "2", "3", "group1", "group2")
                .sorted().toList();
        assertEquals(expectedList, actualList);
    }

    @Test
    @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_APP_ICONS)
    public void testPurgesNotificationIconStyleProviderCache() {
        // GIVEN a notification list
        NotificationEntry entry1 = getNotificationEntryBuilder().setPkg("1").build();
        NotificationEntry entry2 = getNotificationEntryBuilder().setPkg("2").build();
        NotificationEntry entry2bis = getNotificationEntryBuilder().setPkg("2").build();
        NotificationEntry entry3 = getNotificationEntryBuilder().setPkg("3").build();

        String groupKey1 = "group1";
        NotificationEntry summary =
                getNotificationEntryBuilder()
                        .setPkg(groupKey1)
                        .setGroup(mContext, groupKey1)
                        .setGroupSummary(mContext, true)
                        .build();
        NotificationEntry child1 = getNotificationEntryBuilder().setGroup(mContext, groupKey1)
                .setPkg(groupKey1).build();
        NotificationEntry child2 = getNotificationEntryBuilder().setGroup(mContext, groupKey1)
                .setPkg(groupKey1).build();
        GroupEntry groupWithSummaryAndChildren = getGroupEntryBuilder().setKey(groupKey1)
                .setSummary(summary).addChild(child1).addChild(child2).build();

        String groupKey2 = "group2";
        NotificationEntry summary2 =
                getNotificationEntryBuilder()
                        .setPkg(groupKey2)
                        .setGroup(mContext, groupKey2)
                        .setGroupSummary(mContext, true)
                        .build();
        GroupEntry summaryOnlyGroup = getGroupEntryBuilder().setKey(groupKey2)
                .setSummary(summary2).build();

        // WHEN onBeforeTransformGroup is called
        mBeforeTransformGroupsListener.onBeforeTransformGroups(
                List.of(entry1, entry2, entry2bis, entry3,
                        groupWithSummaryAndChildren, summaryOnlyGroup));

        // THEN purge should be called
        ArgumentCaptor<Collection<String>> argumentCaptor = ArgumentCaptor.forClass(List.class);
        verify(mNotificationIconStyleProvider).purgeCache(argumentCaptor.capture());
        List<String> actualList = argumentCaptor.getValue().stream().sorted().toList();
        List<String> expectedList = Stream.of("1", "2", "3", "group1", "group2")
                .sorted().toList();
        assertEquals(expectedList, actualList);
    }

    @Test
    public void testInflatesNewNotification() {
        // WHEN there is a new notification
+4 −1
Original line number Diff line number Diff line
@@ -35,6 +35,9 @@ import java.util.concurrent.atomic.AtomicInteger
 * This cache is safe for multithreaded usage, and is recommended for objects that take a while to
 * resolve (such as drawables, or things that require binder calls). As such, [getOrFetch] is
 * recommended to be run on a background thread, while [purge] can be done from any thread.
 *
 * Important: This cache does NOT have a maximum size, cleaning it up (via [purge]) is the
 * responsibility of the caller, to avoid keeping things in memory unnecessarily.
 */
@SuppressLint("DumpableNotRegistered") // this will be dumped by container classes
class NotifCollectionCache<V>(
@@ -151,7 +154,7 @@ class NotifCollectionCache<V>(
     * purge((c));    // deletes a from the cache and marks b for deletion, etc.
     * ```
     */
    fun purge(wantedKeys: List<String>) {
    fun purge(wantedKeys: Collection<String>) {
        for ((key, entry) in cache) {
            if (key in wantedKeys) {
                entry.resetLives()
+43 −1
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.os.Trace;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -49,13 +50,18 @@ import com.android.systemui.statusbar.notification.collection.render.NotifViewBa
import com.android.systemui.statusbar.notification.collection.render.NotifViewController;
import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager.NotifInflationErrorListener;
import com.android.systemui.statusbar.notification.row.icon.AppIconProvider;
import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;

@@ -104,6 +110,8 @@ public class PreparationCoordinator implements Coordinator {
    /** How long we can delay a group while waiting for all children to inflate */
    private final long mMaxGroupInflationDelay;
    private final BindEventManagerImpl mBindEventManager;
    private final AppIconProvider mAppIconProvider;
    private final NotificationIconStyleProvider mNotificationIconStyleProvider;

    @Inject
    public PreparationCoordinator(
@@ -113,7 +121,9 @@ public class PreparationCoordinator implements Coordinator {
            NotifViewBarn viewBarn,
            NotifUiAdjustmentProvider adjustmentProvider,
            IStatusBarService service,
            BindEventManagerImpl bindEventManager) {
            BindEventManagerImpl bindEventManager,
            AppIconProvider appIconProvider,
            NotificationIconStyleProvider notificationIconStyleProvider) {
        this(
                logger,
                notifInflater,
@@ -122,6 +132,8 @@ public class PreparationCoordinator implements Coordinator {
                adjustmentProvider,
                service,
                bindEventManager,
                appIconProvider,
                notificationIconStyleProvider,
                CHILD_BIND_CUTOFF,
                MAX_GROUP_INFLATION_DELAY);
    }
@@ -135,6 +147,8 @@ public class PreparationCoordinator implements Coordinator {
            NotifUiAdjustmentProvider adjustmentProvider,
            IStatusBarService service,
            BindEventManagerImpl bindEventManager,
            AppIconProvider appIconProvider,
            NotificationIconStyleProvider notificationIconStyleProvider,
            int childBindCutoff,
            long maxGroupInflationDelay) {
        mLogger = logger;
@@ -146,6 +160,8 @@ public class PreparationCoordinator implements Coordinator {
        mChildBindCutoff = childBindCutoff;
        mMaxGroupInflationDelay = maxGroupInflationDelay;
        mBindEventManager = bindEventManager;
        mAppIconProvider = appIconProvider;
        mNotificationIconStyleProvider = notificationIconStyleProvider;
    }

    @Override
@@ -155,6 +171,9 @@ public class PreparationCoordinator implements Coordinator {
                () -> mNotifInflatingFilter.invalidateList("adjustmentProviderChanged"));

        pipeline.addCollectionListener(mNotifCollectionListener);
        if (android.app.Flags.notificationsRedesignAppIcons()) {
            pipeline.addOnBeforeTransformGroupsListener(this::purgeCaches);
        }
        // Inflate after grouping/sorting since that affects what views to inflate.
        pipeline.addOnBeforeFinalizeFilterListener(this::inflateAllRequiredViews);
        pipeline.addFinalizeFilter(mNotifInflationErrorFilter);
@@ -260,6 +279,29 @@ public class PreparationCoordinator implements Coordinator {
        }
    };

    private void purgeCaches(Collection<ListEntry> entries) {
        Set<String> wantedPackages = getPackages(entries);
        mAppIconProvider.purgeCache(wantedPackages);
        mNotificationIconStyleProvider.purgeCache(wantedPackages);
    }

    /**
     * Get all app packages present in {@param entries}.
     */
    private static @NonNull Set<String> getPackages(Collection<ListEntry> entries) {
        Set<String> packages = new HashSet<>();
        for (ListEntry entry : entries) {
            NotificationEntry notificationEntry = entry.getRepresentativeEntry();
            if (notificationEntry == null) {
                Log.wtf(TAG, "notification entry " + entry.getKey()
                        + " has no representative entry");
                continue;
            }
            packages.add(notificationEntry.getSbn().getPackageName());
        }
        return packages;
    }

    private void inflateAllRequiredViews(List<ListEntry> entries) {
        for (int i = 0, size = entries.size(); i < size; i++) {
            ListEntry entry = entries.get(i);
+60 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.statusbar.notification.row.icon

import android.annotation.WorkerThread
import android.app.ActivityManager
import android.app.Flags
import android.content.Context
@@ -27,20 +28,45 @@ import android.graphics.drawable.Drawable
import android.util.Log
import com.android.internal.R
import com.android.launcher3.icons.BaseIconFactory
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.notification.collection.NotifCollectionCache
import com.android.systemui.util.asIndenting
import com.android.systemui.util.withIncreasedIndent
import dagger.Module
import dagger.Provides
import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Provider

/** A provider used to cache and fetch app icons used by notifications. */
interface AppIconProvider {
    /**
     * Loads the icon corresponding to [packageName] into cache, or fetches it from there if already
     * present. This should only be called from the background.
     */
    @Throws(NameNotFoundException::class)
    @WorkerThread
    fun getOrFetchAppIcon(packageName: String, context: Context): Drawable

    /**
     * Mark all the entries in the cache that are NOT in [wantedPackages] to be cleared. If they're
     * still not needed on the next call of this method (made after a timeout of 1s, in case they
     * happen more frequently than that), they will be purged. This can be done from any thread.
     */
    fun purgeCache(wantedPackages: Collection<String>)
}

@SysUISingleton
class AppIconProviderImpl @Inject constructor(private val sysuiContext: Context) : AppIconProvider {
class AppIconProviderImpl
@Inject
constructor(private val sysuiContext: Context, dumpManager: DumpManager) :
    AppIconProvider, Dumpable {
    init {
        dumpManager.registerNormalDumpable(TAG, this)
    }

    private val iconFactory: BaseIconFactory
        get() {
            val isLowRam = ActivityManager.isLowRamDeviceStatic()
@@ -53,13 +79,42 @@ class AppIconProviderImpl @Inject constructor(private val sysuiContext: Context)
            return BaseIconFactory(sysuiContext, res.configuration.densityDpi, iconSize)
        }

    private val cache = NotifCollectionCache<Drawable>()

    override fun getOrFetchAppIcon(packageName: String, context: Context): Drawable {
        return cache.getOrFetch(packageName) { fetchAppIcon(packageName, context) }
    }

    @WorkerThread
    private fun fetchAppIcon(packageName: String, context: Context): BitmapDrawable {
        val icon = context.packageManager.getApplicationIcon(packageName)
        return BitmapDrawable(
            context.resources,
            iconFactory.createScaledBitmap(icon, BaseIconFactory.MODE_HARDWARE),
        )
    }

    override fun purgeCache(wantedPackages: Collection<String>) {
        cache.purge(wantedPackages)
    }

    override fun dump(pwOrig: PrintWriter, args: Array<out String>) {
        val pw = pwOrig.asIndenting()

        pw.println("cache information:")
        pw.withIncreasedIndent { cache.dump(pw, args) }

        val iconFactory = iconFactory
        pw.println("icon factory information:")
        pw.withIncreasedIndent {
            pw.println("fullResIconDpi = ${iconFactory.fullResIconDpi}")
            pw.println("iconSize = ${iconFactory.iconBitmapSize}")
        }
    }

    companion object {
        const val TAG = "AppIconProviderImpl"
    }
}

class NoOpIconProvider : AppIconProvider {
@@ -71,6 +126,10 @@ class NoOpIconProvider : AppIconProvider {
        Log.wtf(TAG, "NoOpIconProvider should not be used anywhere.")
        return ColorDrawable(Color.WHITE)
    }

    override fun purgeCache(wantedPackages: Collection<String>) {
        Log.wtf(TAG, "NoOpIconProvider should not be used anywhere.")
    }
}

@Module
+46 −2
Original line number Diff line number Diff line
@@ -22,9 +22,15 @@ import android.content.Context
import android.content.pm.ApplicationInfo
import android.service.notification.StatusBarNotification
import android.util.Log
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.notification.collection.NotifCollectionCache
import com.android.systemui.util.asIndenting
import com.android.systemui.util.withIncreasedIndent
import dagger.Module
import dagger.Provides
import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Provider

@@ -33,15 +39,35 @@ import javax.inject.Provider
 * notifications.
 */
interface NotificationIconStyleProvider {
    /**
     * Determines whether the [notification] should display the app icon instead of the small icon.
     * This can result in a binder call, and therefore should only be called from the background.
     */
    @WorkerThread
    fun shouldShowAppIcon(notification: StatusBarNotification, context: Context): Boolean

    /**
     * Mark all the entries in the cache that are NOT in [wantedPackages] to be cleared. If they're
     * still not needed on the next call of this method (made after a timeout of 1s, in case they
     * happen more frequently than that), they will be purged. This can be done from any thread.
     */
    fun purgeCache(wantedPackages: Collection<String>)
}

@SysUISingleton
class NotificationIconStyleProviderImpl @Inject constructor() : NotificationIconStyleProvider {
class NotificationIconStyleProviderImpl @Inject constructor(dumpManager: DumpManager) :
    NotificationIconStyleProvider, Dumpable {
    init {
        dumpManager.registerNormalDumpable(TAG, this)
    }

    private val cache = NotifCollectionCache<Boolean>()

    override fun shouldShowAppIcon(notification: StatusBarNotification, context: Context): Boolean {
        val packageContext = notification.getPackageContext(context)
        return !belongsToHeadlessSystemApp(packageContext)
        return cache.getOrFetch(notification.packageName) {
            !belongsToHeadlessSystemApp(packageContext)
        }
    }

    @WorkerThread
@@ -62,6 +88,20 @@ class NotificationIconStyleProviderImpl @Inject constructor() : NotificationIcon
            return false
        }
    }

    override fun purgeCache(wantedPackages: Collection<String>) {
        cache.purge(wantedPackages)
    }

    override fun dump(pwOrig: PrintWriter, args: Array<out String>) {
        val pw = pwOrig.asIndenting()
        pw.println("cache information:")
        pw.withIncreasedIndent { cache.dump(pw, args) }
    }

    companion object {
        const val TAG = "NotificationIconStyleProviderImpl"
    }
}

class NoOpIconStyleProvider : NotificationIconStyleProvider {
@@ -73,6 +113,10 @@ class NoOpIconStyleProvider : NotificationIconStyleProvider {
        Log.wtf(TAG, "NoOpIconStyleProvider should not be used anywhere.")
        return true
    }

    override fun purgeCache(wantedPackages: Collection<String>) {
        Log.wtf(TAG, "NoOpIconStyleProvider should not be used anywhere.")
    }
}

@Module
Loading