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

Commit 2520ac32 authored by Beverly's avatar Beverly
Browse files

Add HeadsUpCoordinator to new notif pipeline

Fixes: 150797570
Test: atest HeadsUpCoordinatorTest PreparationCoordinatorTest
Change-Id: I42aa9008deb930a3b647a81428c1911171a1fe10
parent 3b838926
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);
    }
}