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

Commit 97bec1d9 authored by Beverly Tai's avatar Beverly Tai Committed by Android (Google) Code Review
Browse files

Merge "Add HeadsUpCoordinator to new notif pipeline" into rvc-dev

parents 927d128d 2520ac32
Loading
Loading
Loading
Loading
+171 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.statusbar.notification.collection.coordinator;

import static com.android.systemui.statusbar.NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY;

import android.annotation.Nullable;

import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;

import java.util.Objects;

import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * Coordinates heads up notification (HUN) interactions with the notification pipeline based on
 * the HUN state reported by the {@link HeadsUpManager}. In this class we only consider one
 * notification, in particular the {@link HeadsUpManager#getTopEntry()}, to be HeadsUpping at a
 * time even though other notifications may be queued to heads up next.
 *
 * The current HUN, but not HUNs that are queued to heads up, will be:
 * - Lifetime extended until it's no longer heads upping.
 * - Promoted out of its group if it's a child of a group.
 * - In the HeadsUpCoordinatorSection. Ordering is configured in {@link NotifCoordinators}.
 * - Removed from HeadsUpManager if it's removed from the NotificationCollection.
 *
 * Note: The inflation callback in {@link PreparationCoordinator} handles showing HUNs.
 */
@Singleton
public class HeadsUpCoordinator implements Coordinator {
    private static final String TAG = "HeadsUpCoordinator";

    private final HeadsUpManager mHeadsUpManager;
    private final NotificationRemoteInputManager mRemoteInputManager;

    // tracks the current HeadUpNotification reported by HeadsUpManager
    private @Nullable NotificationEntry mCurrentHun;

    private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension;
    private NotificationEntry mNotifExtendingLifetime; // notif we've extended the lifetime for

    @Inject
    public HeadsUpCoordinator(
            HeadsUpManager headsUpManager,
            NotificationRemoteInputManager remoteInputManager) {
        mHeadsUpManager = headsUpManager;
        mRemoteInputManager = remoteInputManager;
    }

    @Override
    public void attach(NotifPipeline pipeline) {
        mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
        pipeline.addCollectionListener(mNotifCollectionListener);
        pipeline.addPromoter(mNotifPromoter);
        pipeline.addNotificationLifetimeExtender(mLifetimeExtender);
    }

    @Override
    public NotifSection getSection() {
        return mNotifSection;
    }

    private final NotifCollectionListener mNotifCollectionListener = new NotifCollectionListener() {
        /**
         * Stop alerting HUNs that are removed from the notification collection
         */
        @Override
        public void onEntryRemoved(NotificationEntry entry, int reason) {
            final String entryKey = entry.getKey();
            if (mHeadsUpManager.isAlerting(entryKey)) {
                boolean removeImmediatelyForRemoteInput =
                        mRemoteInputManager.getController().isSpinning(entryKey)
                                && !FORCE_REMOTE_INPUT_HISTORY;
                mHeadsUpManager.removeNotification(entry.getKey(), removeImmediatelyForRemoteInput);
            }
        }
    };

    private final NotifLifetimeExtender mLifetimeExtender = new NotifLifetimeExtender() {
        @Override
        public String getName() {
            return TAG;
        }

        @Override
        public void setCallback(OnEndLifetimeExtensionCallback callback) {
            mEndLifetimeExtension = callback;
        }

        @Override
        public boolean shouldExtendLifetime(NotificationEntry entry, int reason) {
            boolean isShowingHun = isCurrentlyShowingHun(entry);
            if (isShowingHun) {
                mNotifExtendingLifetime = entry;
            }
            return isShowingHun;
        }

        @Override
        public void cancelLifetimeExtension(NotificationEntry entry) {
            if (Objects.equals(mNotifExtendingLifetime, entry)) {
                mNotifExtendingLifetime = null;
            }
        }
    };

    private final NotifPromoter mNotifPromoter = new NotifPromoter(TAG) {
        @Override
        public boolean shouldPromoteToTopLevel(NotificationEntry entry) {
            return isCurrentlyShowingHun(entry);
        }
    };

    private final NotifSection mNotifSection = new NotifSection(TAG) {
        @Override
        public boolean isInSection(ListEntry entry) {
            return isCurrentlyShowingHun(entry);
        }
    };

    private final OnHeadsUpChangedListener mOnHeadsUpChangedListener =
            new OnHeadsUpChangedListener() {
        @Override
        public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
            NotificationEntry newHUN = mHeadsUpManager.getTopEntry();
            if (!Objects.equals(mCurrentHun, newHUN)) {
                endNotifLifetimeExtension();
                mCurrentHun = newHUN;
                mNotifPromoter.invalidateList();
                mNotifSection.invalidateList();
            }
        }
    };

    private boolean isCurrentlyShowingHun(ListEntry entry) {
        return mCurrentHun == entry.getRepresentativeEntry();
    }

    private void endNotifLifetimeExtension() {
        if (mNotifExtendingLifetime != null) {
            mEndLifetimeExtension.onEndLifetimeExtension(
                    mLifetimeExtender,
                    mNotifExtendingLifetime);
            mNotifExtendingLifetime = null;
        }
    }
}
+3 −2
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ public class NotifCoordinators implements Dumpable {
    public NotifCoordinators(
            DumpManager dumpManager,
            FeatureFlags featureFlags,
            HeadsUpCoordinator headsUpCoordinator,
            KeyguardCoordinator keyguardCoordinator,
            RankingCoordinator rankingCoordinator,
            ForegroundCoordinator foregroundCoordinator,
@@ -56,7 +57,6 @@ public class NotifCoordinators implements Dumpable {
            BubbleCoordinator bubbleCoordinator,
            PreparationCoordinator preparationCoordinator) {
        dumpManager.registerDumpable(TAG, this);

        mCoordinators.add(new HideLocallyDismissedNotifsCoordinator());
        mCoordinators.add(keyguardCoordinator);
        mCoordinators.add(rankingCoordinator);
@@ -64,9 +64,10 @@ public class NotifCoordinators implements Dumpable {
        mCoordinators.add(deviceProvisionedCoordinator);
        mCoordinators.add(bubbleCoordinator);
        if (featureFlags.isNewNotifPipelineRenderingEnabled()) {
            mCoordinators.add(headsUpCoordinator);
            mCoordinators.add(preparationCoordinator);
        }
        // TODO: add new Coordinators here! (b/145134683, b/112656837)
        // TODO: add new Coordinators here! (b/112656837)

        // TODO: add the sections in a particular ORDER (HeadsUp < People < Alerting)
        for (Coordinator c : mCoordinators) {
+15 −1
Original line number Diff line number Diff line
@@ -33,7 +33,9 @@ import com.android.systemui.statusbar.notification.collection.inflation.NotifInf
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener;
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.interruption.NotificationInterruptStateProvider;
import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
import com.android.systemui.statusbar.policy.HeadsUpManager;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -61,6 +63,8 @@ public class PreparationCoordinator implements Coordinator {
    private final NotifViewBarn mViewBarn;
    private final Map<NotificationEntry, Integer> mInflationStates = new ArrayMap<>();
    private final IStatusBarService mStatusBarService;
    private final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
    private final HeadsUpManager mHeadsUpManager;

    @Inject
    public PreparationCoordinator(
@@ -68,7 +72,10 @@ public class PreparationCoordinator implements Coordinator {
            NotifInflaterImpl notifInflater,
            NotifInflationErrorManager errorManager,
            NotifViewBarn viewBarn,
            IStatusBarService service) {
            IStatusBarService service,
            NotificationInterruptStateProvider notificationInterruptStateProvider,
            HeadsUpManager headsUpManager
    ) {
        mLogger = logger;
        mNotifInflater = notifInflater;
        mNotifInflater.setInflationCallback(mInflationCallback);
@@ -76,6 +83,8 @@ public class PreparationCoordinator implements Coordinator {
        mNotifErrorManager.addInflationErrorListener(mInflationErrorListener);
        mViewBarn = viewBarn;
        mStatusBarService = service;
        mNotificationInterruptStateProvider = notificationInterruptStateProvider;
        mHeadsUpManager = headsUpManager;
    }

    @Override
@@ -149,6 +158,11 @@ public class PreparationCoordinator implements Coordinator {
            mLogger.logNotifInflated(entry.getKey());
            mViewBarn.registerViewForEntry(entry, entry.getRow());
            mInflationStates.put(entry, STATE_INFLATED);

            // TODO: should eventually be moved to HeadsUpCoordinator
            if (mNotificationInterruptStateProvider.shouldHeadsUp(entry)) {
                mHeadsUpManager.showNotification(entry);
            }
            mNotifInflatingFilter.invalidateList();
        }
    };
+191 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.statusbar.notification.collection.coordinator;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;

import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.RemoteInputController;
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.NotifPromoter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class HeadsUpCoordinatorTest extends SysuiTestCase {

    private HeadsUpCoordinator mCoordinator;

    // captured listeners and pluggables:
    private NotifCollectionListener mCollectionListener;
    private NotifPromoter mNotifPromoter;
    private NotifLifetimeExtender mNotifLifetimeExtender;
    private OnHeadsUpChangedListener mOnHeadsUpChangedListener;
    private NotifSection mNotifSection;

    @Mock private NotifPipeline mNotifPipeline;
    @Mock private HeadsUpManager mHeadsUpManager;
    @Mock private NotificationRemoteInputManager mRemoteInputManager;
    @Mock private RemoteInputController mRemoteInputController;
    @Mock private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension;

    private NotificationEntry mEntry;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController);

        mCoordinator = new HeadsUpCoordinator(
                mHeadsUpManager,
                mRemoteInputManager
        );

        mCoordinator.attach(mNotifPipeline);

        // capture arguments:
        ArgumentCaptor<NotifCollectionListener> notifCollectionCaptor =
                ArgumentCaptor.forClass(NotifCollectionListener.class);
        ArgumentCaptor<NotifPromoter> notifPromoterCaptor =
                ArgumentCaptor.forClass(NotifPromoter.class);
        ArgumentCaptor<NotifLifetimeExtender> notifLifetimeExtenderCaptor =
                ArgumentCaptor.forClass(NotifLifetimeExtender.class);
        ArgumentCaptor<OnHeadsUpChangedListener> headsUpChangedListenerCaptor =
                ArgumentCaptor.forClass(OnHeadsUpChangedListener.class);

        verify(mNotifPipeline).addCollectionListener(notifCollectionCaptor.capture());
        verify(mNotifPipeline).addPromoter(notifPromoterCaptor.capture());
        verify(mNotifPipeline).addNotificationLifetimeExtender(
                notifLifetimeExtenderCaptor.capture());
        verify(mHeadsUpManager).addListener(headsUpChangedListenerCaptor.capture());

        mCollectionListener = notifCollectionCaptor.getValue();
        mNotifPromoter = notifPromoterCaptor.getValue();
        mNotifLifetimeExtender = notifLifetimeExtenderCaptor.getValue();
        mOnHeadsUpChangedListener = headsUpChangedListenerCaptor.getValue();

        mNotifSection = mCoordinator.getSection();
        mNotifLifetimeExtender.setCallback(mEndLifetimeExtension);
        mEntry = new NotificationEntryBuilder().build();
    }

    @Test
    public void testPromotesCurrentHUN() {
        // GIVEN the current HUN is set to mEntry
        setCurrentHUN(mEntry);

        // THEN only promote the current HUN, mEntry
        assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry));
        assertFalse(mNotifPromoter.shouldPromoteToTopLevel(new NotificationEntryBuilder().build()));
    }

    @Test
    public void testIncludeInSectionCurrentHUN() {
        // GIVEN the current HUN is set to mEntry
        setCurrentHUN(mEntry);

        // THEN only section the current HUN, mEntry
        assertTrue(mNotifSection.isInSection(mEntry));
        assertFalse(mNotifSection.isInSection(new NotificationEntryBuilder().build()));
    }

    @Test
    public void testLifetimeExtendsCurrentHUN() {
        // GIVEN there is a HUN, mEntry
        setCurrentHUN(mEntry);

        // THEN only the current HUN, mEntry, should be lifetimeExtended
        assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(mEntry, /* cancellationReason */ 0));
        assertFalse(mNotifLifetimeExtender.shouldExtendLifetime(
                new NotificationEntryBuilder().build(), /* cancellationReason */ 0));
    }

    @Test
    public void testLifetimeExtensionEndsOnNewHUN() {
        // GIVEN there was a HUN that was lifetime extended
        setCurrentHUN(mEntry);
        assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(
                mEntry, /* cancellation reason */ 0));

        // WHEN there's a new HUN
        NotificationEntry newHUN = new NotificationEntryBuilder().build();
        setCurrentHUN(newHUN);

        // THEN the old entry's lifetime extension should be cancelled
        verify(mEndLifetimeExtension).onEndLifetimeExtension(mNotifLifetimeExtender, mEntry);
    }

    @Test
    public void testLifetimeExtensionEndsOnNoHUNs() {
        // GIVEN there was a HUN that was lifetime extended
        setCurrentHUN(mEntry);
        assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(
                mEntry, /* cancellation reason */ 0));

        // WHEN there's no longer a HUN
        setCurrentHUN(null);

        // THEN the old entry's lifetime extension should be cancelled
        verify(mEndLifetimeExtension).onEndLifetimeExtension(mNotifLifetimeExtender, mEntry);
    }

    @Test
    public void testOnEntryRemovedRemovesHeadsUpNotification() {
        // GIVEN the current HUN is mEntry
        setCurrentHUN(mEntry);

        // WHEN mEntry is removed from the notification collection
        mCollectionListener.onEntryRemoved(mEntry, /* cancellation reason */ 0);
        when(mRemoteInputController.isSpinning(any())).thenReturn(false);

        // THEN heads up manager should remove the entry
        verify(mHeadsUpManager).removeNotification(mEntry.getKey(), false);
    }

    private void setCurrentHUN(NotificationEntry entry) {
        when(mHeadsUpManager.getTopEntry()).thenReturn(entry);
        when(mHeadsUpManager.isAlerting(any())).thenReturn(false);
        if (entry != null) {
            when(mHeadsUpManager.isAlerting(entry.getKey())).thenReturn(true);
        }
        mOnHeadsUpChangedListener.onHeadsUpStateChanged(entry, entry != null);
    }
}
+29 −1
Original line number Diff line number Diff line
@@ -20,8 +20,10 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
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.os.RemoteException;
import android.testing.AndroidTestingRunner;
@@ -39,7 +41,9 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener;
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.interruption.NotificationInterruptStateProvider;
import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
import com.android.systemui.statusbar.policy.HeadsUpManager;

import org.junit.Before;
import org.junit.Test;
@@ -74,6 +78,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
    @Mock private NotifPipeline mNotifPipeline;
    @Mock private IStatusBarService mService;
    @Mock private NotifInflaterImpl mNotifInflater;
    @Mock private NotificationInterruptStateProvider mNotificationInterruptStateProvider;
    @Mock private HeadsUpManager mHeadsUpManager;

    @Before
    public void setUp() {
@@ -88,7 +94,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
                mNotifInflater,
                mErrorManager,
                mock(NotifViewBarn.class),
                mService);
                mService,
                mNotificationInterruptStateProvider,
                mHeadsUpManager);

        ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class);
        mCoordinator.attach(mNotifPipeline);
@@ -172,4 +180,24 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
        // THEN it isn't filtered from shade list
        assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
    }

    @Test
    public void testShowHUNOnInflationFinished() {
        // WHEN a notification should HUN and its inflation is finished
        when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true);
        mCallback.onInflationFinished(mEntry);

        // THEN we tell the HeadsUpManager to show the notification
        verify(mHeadsUpManager).showNotification(mEntry);
    }

    @Test
    public void testNoHUNOnInflationFinished() {
        // WHEN a notification shouldn't HUN and its inflation is finished
        when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(false);
        mCallback.onInflationFinished(mEntry);

        // THEN we never tell the HeadsUpManager to show the notification
        verify(mHeadsUpManager, never()).showNotification(mEntry);
    }
}