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

Commit 29ce997a authored by Julia Tuttle's avatar Julia Tuttle
Browse files

Inflate media notification icons in MediaCoordinator

In the old pipeline, notifications were inflated before the media
notification(s) were filtered out and used to populate the quick
settings, lockscreen, or AOD media views.

In the new pipeline, MediaCoordinator filters out media notifications
before PreparationCoordinator inflates them, so they're never inflated.

This is mostly okay, except that the AOD (and maybe lockscreen?) media
views use one of the media notification's icons to label the media
metadata, and that icon was no longer getting inflated.

Therefore, have MediaCoordinator do the necessary subset of the work of
PreparationCoordinator for the media notifications it filters out:
create and update icons for notifications it filters out, report
inflation errors back to StatusBarService, and do the bookkeeping
necessary to do the right thing (create, update, or nothing) each time
we see a notification.

Bug: 228397680
Test: atest MediaCoordinatorTest (added some test cases)
Change-Id: Ic42c276e5a973cdd260a8c4729a5abdf53987199
parent 2274df1e
Loading
Loading
Loading
Loading
+91 −2
Original line number Diff line number Diff line
@@ -18,11 +18,19 @@ package com.android.systemui.statusbar.notification.collection.coordinator;

import static com.android.systemui.media.MediaDataManagerKt.isMediaNotification;

import android.os.RemoteException;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;

import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.media.MediaFeatureFlag;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.icon.IconManager;

import javax.inject.Inject;

@@ -34,21 +42,102 @@ public class MediaCoordinator implements Coordinator {
    private static final String TAG = "MediaCoordinator";

    private final Boolean mIsMediaFeatureEnabled;
    private final IStatusBarService mStatusBarService;
    private final IconManager mIconManager;

    private static final int STATE_ICONS_UNINFLATED = 0;
    private static final int STATE_ICONS_INFLATED = 1;
    private static final int STATE_ICONS_ERROR = 2;

    private final ArrayMap<NotificationEntry, Integer> mIconsState = new ArrayMap<>();

    private final NotifFilter mMediaFilter = new NotifFilter(TAG) {
        @Override
        public boolean shouldFilterOut(NotificationEntry entry, long now) {
            return mIsMediaFeatureEnabled && isMediaNotification(entry.getSbn());
            if (!mIsMediaFeatureEnabled || !isMediaNotification(entry.getSbn())) {
                return false;
            }

            switch (mIconsState.getOrDefault(entry, STATE_ICONS_UNINFLATED)) {
                case STATE_ICONS_UNINFLATED:
                    try {
                        mIconManager.createIcons(entry);
                        mIconsState.put(entry, STATE_ICONS_INFLATED);
                    } catch (InflationException e) {
                        reportInflationError(entry, e);
                        mIconsState.put(entry, STATE_ICONS_ERROR);
                    }
                    break;
                case STATE_ICONS_INFLATED:
                    try {
                        mIconManager.updateIcons(entry);
                    } catch (InflationException e) {
                        reportInflationError(entry, e);
                        mIconsState.put(entry, STATE_ICONS_ERROR);
                    }
                    break;
                case STATE_ICONS_ERROR:
                    // do nothing
                    break;
            }

            return true;
        }
    };

    private final NotifCollectionListener mCollectionListener = new NotifCollectionListener() {
        @Override
        public void onEntryInit(NotificationEntry entry) {
            mIconsState.put(entry, STATE_ICONS_UNINFLATED);
        }

        @Override
        public void onEntryUpdated(NotificationEntry entry) {
            if (mIconsState.getOrDefault(entry, STATE_ICONS_UNINFLATED) == STATE_ICONS_ERROR) {
                // The update may have fixed the inflation error, so give it another chance.
                mIconsState.put(entry, STATE_ICONS_UNINFLATED);
            }
        }

        @Override
        public void onEntryCleanUp(NotificationEntry entry) {
            mIconsState.remove(entry);
        }
    };

    private void reportInflationError(NotificationEntry entry, Exception e) {
        // This is the same logic as in PreparationCoordinator; it doesn't handle media
        // notifications when the media feature is enabled since they aren't displayed in the shade,
        // so we have to handle inflating the icons (for AOD, at the very least) and reporting any
        // errors ourselves.
        try {
            final StatusBarNotification sbn = entry.getSbn();
            // report notification inflation errors back up
            // to notification delegates
            mStatusBarService.onNotificationError(
                    sbn.getPackageName(),
                    sbn.getTag(),
                    sbn.getId(),
                    sbn.getUid(),
                    sbn.getInitialPid(),
                    e.getMessage(),
                    sbn.getUser().getIdentifier());
        } catch (RemoteException ex) {
            // System server is dead, nothing to do about that
        }
    }

    @Inject
    public MediaCoordinator(MediaFeatureFlag featureFlag) {
    public MediaCoordinator(MediaFeatureFlag featureFlag, IStatusBarService statusBarService,
            IconManager iconManager) {
        mIsMediaFeatureEnabled = featureFlag.getEnabled();
        mStatusBarService = statusBarService;
        mIconManager = iconManager;
    }

    @Override
    public void attach(NotifPipeline pipeline) {
        pipeline.addPreGroupFilter(mMediaFilter);
        pipeline.addCollectionListener(mCollectionListener);
    }
}
+118 −19
Original line number Diff line number Diff line
@@ -18,21 +18,31 @@ package com.android.systemui.statusbar.notification.collection.coordinator;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Notification.MediaStyle;
import android.media.session.MediaSession;
import android.service.notification.NotificationListenerService;
import android.testing.AndroidTestingRunner;

import androidx.test.filters.SmallTest;

import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.media.MediaFeatureFlag;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.icon.IconManager;

import org.junit.After;
import org.junit.Before;
@@ -52,6 +62,12 @@ public final class MediaCoordinatorTest extends SysuiTestCase {

    @Mock private NotifPipeline mNotifPipeline;
    @Mock private MediaFeatureFlag mMediaFeatureFlag;
    @Mock private IStatusBarService mStatusBarService;
    @Mock private IconManager mIconManager;

    private MediaCoordinator mCoordinator;
    private NotifFilter mFilter;
    private NotifCollectionListener mListener;

    @Before
    public void setUp() {
@@ -72,11 +88,9 @@ public final class MediaCoordinatorTest extends SysuiTestCase {
    @Test
    public void shouldFilterOtherNotificationWhenDisabled() {
        // GIVEN that the media feature is disabled
        when(mMediaFeatureFlag.getEnabled()).thenReturn(false);
        MediaCoordinator coordinator = new MediaCoordinator(mMediaFeatureFlag);
        finishSetupWithMediaFeatureFlagEnabled(false);
        // WHEN the media filter is asked about an entry
        NotifFilter filter = captureFilter(coordinator);
        final boolean shouldFilter = filter.shouldFilterOut(mOtherEntry, 0);
        final boolean shouldFilter = mFilter.shouldFilterOut(mOtherEntry, 0);
        // THEN it shouldn't be filtered
        assertThat(shouldFilter).isFalse();
    }
@@ -84,11 +98,9 @@ public final class MediaCoordinatorTest extends SysuiTestCase {
    @Test
    public void shouldFilterOtherNotificationWhenEnabled() {
        // GIVEN that the media feature is enabled
        when(mMediaFeatureFlag.getEnabled()).thenReturn(true);
        MediaCoordinator coordinator = new MediaCoordinator(mMediaFeatureFlag);
        finishSetupWithMediaFeatureFlagEnabled(true);
        // WHEN the media filter is asked about an entry
        NotifFilter filter = captureFilter(coordinator);
        final boolean shouldFilter = filter.shouldFilterOut(mOtherEntry, 0);
        final boolean shouldFilter = mFilter.shouldFilterOut(mOtherEntry, 0);
        // THEN it shouldn't be filtered
        assertThat(shouldFilter).isFalse();
    }
@@ -96,11 +108,9 @@ public final class MediaCoordinatorTest extends SysuiTestCase {
    @Test
    public void shouldFilterMediaNotificationWhenDisabled() {
        // GIVEN that the media feature is disabled
        when(mMediaFeatureFlag.getEnabled()).thenReturn(false);
        MediaCoordinator coordinator = new MediaCoordinator(mMediaFeatureFlag);
        finishSetupWithMediaFeatureFlagEnabled(false);
        // WHEN the media filter is asked about a media entry
        NotifFilter filter = captureFilter(coordinator);
        final boolean shouldFilter = filter.shouldFilterOut(mMediaEntry, 0);
        final boolean shouldFilter = mFilter.shouldFilterOut(mMediaEntry, 0);
        // THEN it shouldn't be filtered
        assertThat(shouldFilter).isFalse();
    }
@@ -108,19 +118,108 @@ public final class MediaCoordinatorTest extends SysuiTestCase {
    @Test
    public void shouldFilterMediaNotificationWhenEnabled() {
        // GIVEN that the media feature is enabled
        when(mMediaFeatureFlag.getEnabled()).thenReturn(true);
        MediaCoordinator coordinator = new MediaCoordinator(mMediaFeatureFlag);
        finishSetupWithMediaFeatureFlagEnabled(true);
        // WHEN the media filter is asked about a media entry
        NotifFilter filter = captureFilter(coordinator);
        final boolean shouldFilter = filter.shouldFilterOut(mMediaEntry, 0);
        final boolean shouldFilter = mFilter.shouldFilterOut(mMediaEntry, 0);
        // THEN it should be filtered
        assertThat(shouldFilter).isTrue();
    }

    private NotifFilter captureFilter(MediaCoordinator coordinator) {
    @Test
    public void inflateNotificationIconsMediaDisabled() throws InflationException {
        finishSetupWithMediaFeatureFlagEnabled(false);

        mListener.onEntryInit(mOtherEntry);
        mFilter.shouldFilterOut(mOtherEntry, 0);
        verify(mIconManager, never()).createIcons(eq(mMediaEntry));
    }

    @Test
    public void inflateNotificationIconsMediaEnabled() throws InflationException {
        finishSetupWithMediaFeatureFlagEnabled(true);

        mListener.onEntryInit(mOtherEntry);
        mFilter.shouldFilterOut(mOtherEntry, 0);
        verify(mIconManager, never()).createIcons(eq(mMediaEntry));
    }

    @Test
    public void inflateMediaNotificationIconsMediaDisabled() throws InflationException {
        finishSetupWithMediaFeatureFlagEnabled(false);

        mListener.onEntryInit(mMediaEntry);
        mFilter.shouldFilterOut(mMediaEntry, 0);
        verify(mIconManager, never()).createIcons(eq(mMediaEntry));
    }

    @Test
    public void inflateMediaNotificationIconsMediaEnabled() throws InflationException {
        finishSetupWithMediaFeatureFlagEnabled(true);

        mListener.onEntryInit(mMediaEntry);
        mListener.onEntryAdded(mMediaEntry);
        verify(mIconManager, never()).createIcons(eq(mMediaEntry));
        verify(mIconManager, never()).updateIcons(eq(mMediaEntry));

        mFilter.shouldFilterOut(mMediaEntry, 0);
        verify(mIconManager, times(1)).createIcons(eq(mMediaEntry));
        verify(mIconManager, never()).updateIcons(eq(mMediaEntry));

        mFilter.shouldFilterOut(mMediaEntry, 0);
        verify(mIconManager, times(1)).createIcons(eq(mMediaEntry));
        verify(mIconManager, times(1)).updateIcons(eq(mMediaEntry));

        mListener.onEntryRemoved(mMediaEntry, NotificationListenerService.REASON_CANCEL);
        mListener.onEntryCleanUp(mMediaEntry);
        mListener.onEntryInit(mMediaEntry);
        verify(mIconManager, times(1)).createIcons(eq(mMediaEntry));
        verify(mIconManager, times(1)).updateIcons(eq(mMediaEntry));

        mFilter.shouldFilterOut(mMediaEntry, 0);
        verify(mIconManager, times(2)).createIcons(eq(mMediaEntry));
        verify(mIconManager, times(1)).updateIcons(eq(mMediaEntry));
    }

    @Test
    public void inflationException() throws InflationException {
        finishSetupWithMediaFeatureFlagEnabled(true);

        mListener.onEntryInit(mMediaEntry);
        mListener.onEntryAdded(mMediaEntry);
        verify(mIconManager, never()).createIcons(eq(mMediaEntry));
        verify(mIconManager, never()).updateIcons(eq(mMediaEntry));

        doThrow(InflationException.class).when(mIconManager).createIcons(eq(mMediaEntry));
        mFilter.shouldFilterOut(mMediaEntry, 0);
        verify(mIconManager, times(1)).createIcons(eq(mMediaEntry));
        verify(mIconManager, never()).updateIcons(eq(mMediaEntry));

        mFilter.shouldFilterOut(mMediaEntry, 0);
        verify(mIconManager, times(1)).createIcons(eq(mMediaEntry));
        verify(mIconManager, never()).updateIcons(eq(mMediaEntry));

        mListener.onEntryUpdated(mMediaEntry);
        verify(mIconManager, times(1)).createIcons(eq(mMediaEntry));
        verify(mIconManager, never()).updateIcons(eq(mMediaEntry));

        doNothing().when(mIconManager).createIcons(eq(mMediaEntry));
        mFilter.shouldFilterOut(mMediaEntry, 0);
        verify(mIconManager, times(2)).createIcons(eq(mMediaEntry));
        verify(mIconManager, never()).updateIcons(eq(mMediaEntry));
    }

    private void finishSetupWithMediaFeatureFlagEnabled(boolean mediaFeatureFlagEnabled) {
        when(mMediaFeatureFlag.getEnabled()).thenReturn(mediaFeatureFlagEnabled);
        mCoordinator = new MediaCoordinator(mMediaFeatureFlag, mStatusBarService, mIconManager);

        ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class);
        coordinator.attach(mNotifPipeline);
        ArgumentCaptor<NotifCollectionListener> listenerCaptor =
                ArgumentCaptor.forClass(NotifCollectionListener.class);
        mCoordinator.attach(mNotifPipeline);
        verify(mNotifPipeline).addPreGroupFilter(filterCaptor.capture());
        return filterCaptor.getValue();
        verify(mNotifPipeline).addCollectionListener(listenerCaptor.capture());

        mFilter = filterCaptor.getValue();
        mListener = listenerCaptor.getValue();
    }
}