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

Commit 16921422 authored by Jeff DeCew's avatar Jeff DeCew
Browse files

Implement the V2 notification minimalism prototype

* Only notifications in the Media, Heads Up, or FGS buckets can show outside the shelf on the lock screen.
* The top unseen notification is sectioned no lower than the FGS notifications.
* The top unseen notification is (by default) promoted out of its group so that it can be shown on lockscreen.
* Unseen notifications are still tracked the same as with the tangor behavior, but can be seen in the locked shade.

Bug: 330387368
Test: manual
Flag: com.android.systemui.notification_minimalism_prototype
Change-Id: Ic390064d713d5355ce56005b7a61f5d0ee9fd698
parent 8051b0b5
Loading
Loading
Loading
Loading
+5 −2
Original line number Diff line number Diff line
@@ -31,9 +31,9 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

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

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.compose.animation.scene.ObservableTransitionState;
@@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.kotlin.JavaAdapter;
@@ -75,7 +76,7 @@ import org.mockito.MockitoAnnotations;
import org.mockito.verification.VerificationMode;

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

@@ -86,6 +87,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase {
    @Mock private WakefulnessLifecycle mWakefulnessLifecycle;
    @Mock private StatusBarStateController mStatusBarStateController;
    @Mock private Pluggable.PluggableListener<NotifStabilityManager> mInvalidateListener;
    @Mock private SeenNotificationsInteractor mSeenNotificationsInteractor;
    @Mock private HeadsUpManager mHeadsUpManager;
    @Mock private VisibilityLocationProvider mVisibilityLocationProvider;
    @Mock private VisualStabilityProvider mVisualStabilityProvider;
@@ -121,6 +123,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase {
                mHeadsUpManager,
                mShadeAnimationInteractor,
                mJavaAdapter,
                mSeenNotificationsInteractor,
                mStatusBarStateController,
                mVisibilityLocationProvider,
                mVisualStabilityProvider,
+67 −2
Original line number Diff line number Diff line
@@ -31,15 +31,20 @@ import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.expansionChanges
import com.android.systemui.statusbar.notification.collection.GroupEntry
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.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
import com.android.systemui.statusbar.notification.stack.BUCKET_FOREGROUND_SERVICE
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.headsUpEvents
import com.android.systemui.util.asIndenting
@@ -106,6 +111,10 @@ constructor(
    }

    private fun attachUnseenFilter(pipeline: NotifPipeline) {
        if (NotificationMinimalismPrototype.V2.isEnabled) {
            pipeline.addPromoter(unseenNotifPromoter)
            pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotif)
        }
        pipeline.addFinalizeFilter(unseenNotifFilter)
        pipeline.addCollectionListener(collectionListener)
        scope.launch { trackUnseenFilterSettingChanges() }
@@ -263,7 +272,10 @@ constructor(
    }

    private fun unseenFeatureEnabled(): Flow<Boolean> {
        if (NotificationMinimalismPrototype.V1.isEnabled) {
        if (
            NotificationMinimalismPrototype.V1.isEnabled ||
                NotificationMinimalismPrototype.V2.isEnabled
        ) {
            return flowOf(true)
        }
        return secureSettings
@@ -334,6 +346,57 @@ constructor(
            }
        }

    private fun pickOutTopUnseenNotif(list: List<ListEntry>) {
        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
        // Only ever elevate a top unseen notification on keyguard, not even locked shade
        if (statusBarStateController.state != StatusBarState.KEYGUARD) {
            seenNotificationsInteractor.setTopUnseenNotification(null)
            return
        }
        // On keyguard pick the top-ranked unseen or ongoing notification to elevate
        seenNotificationsInteractor.setTopUnseenNotification(
            list
                .asSequence()
                .flatMap {
                    when (it) {
                        is NotificationEntry -> listOfNotNull(it)
                        is GroupEntry -> it.children
                        else -> error("unhandled type of $it")
                    }
                }
                .filter { shouldIgnoreUnseenCheck(it) || it in unseenNotifications }
                .minByOrNull { it.ranking.rank }
        )
    }

    @VisibleForTesting
    internal val unseenNotifPromoter =
        object : NotifPromoter("$TAG-unseen") {
            override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
                if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
                else
                    seenNotificationsInteractor.isTopUnseenNotification(child) &&
                        NotificationMinimalismPrototype.V2.ungroupTopUnseen
        }

    val unseenNotifSectioner =
        object : NotifSectioner("Unseen", BUCKET_FOREGROUND_SERVICE) {
            override fun isInSection(entry: ListEntry): Boolean {
                if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false
                if (
                    seenNotificationsInteractor.isTopUnseenNotification(entry.representativeEntry)
                ) {
                    return true
                }
                if (entry !is GroupEntry) {
                    return false
                }
                return entry.children.any {
                    seenNotificationsInteractor.isTopUnseenNotification(it)
                }
            }
        }

    @VisibleForTesting
    internal val unseenNotifFilter =
        object : NotifFilter("$TAG-unseen") {
@@ -351,7 +414,9 @@ constructor(
             * allow seen notifications to appear in the locked shade.
             */
            private fun isOnKeyguard(): Boolean =
                if (
                if (NotificationMinimalismPrototype.V2.isEnabled) {
                    false // disable this feature under this prototype
                } else if (
                    NotificationMinimalismPrototype.V1.isEnabled &&
                        NotificationMinimalismPrototype.V1.showOnLockedShade
                ) {
+54 −43
Original line number Diff line number Diff line
@@ -24,17 +24,20 @@ import com.android.systemui.statusbar.notification.collection.SortBySectionTimeF
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import javax.inject.Inject

/**
 * Handles the attachment of [Coordinator]s to the [NotifPipeline] so that the
 * Coordinators can register their respective callbacks.
 * Handles the attachment of [Coordinator]s to the [NotifPipeline] so that the Coordinators can
 * register their respective callbacks.
 */
interface NotifCoordinators : Coordinator, PipelineDumpable

@CoordinatorScope
class NotifCoordinatorsImpl @Inject constructor(
class NotifCoordinatorsImpl
@Inject
constructor(
    sectionStyleProvider: SectionStyleProvider,
    featureFlags: FeatureFlags,
    dataStoreCoordinator: DataStoreCoordinator,
@@ -114,6 +117,9 @@ class NotifCoordinatorsImpl @Inject constructor(
        // Manually add Ordered Sections
        mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
        mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
        if (NotificationMinimalismPrototype.V2.isEnabled) {
            mOrderedSections.add(keyguardCoordinator.unseenNotifSectioner) // Unseen (FGS)
        }
        mOrderedSections.add(conversationCoordinator.peopleAlertingSectioner) // People Alerting
        if (!SortBySectionTimeFlag.isEnabled) {
            mOrderedSections.add(conversationCoordinator.peopleSilentSectioner) // People Silent
@@ -124,22 +130,26 @@ class NotifCoordinatorsImpl @Inject constructor(

        sectionStyleProvider.setMinimizedSections(setOf(rankingCoordinator.minimizedSectioner))
        if (SortBySectionTimeFlag.isEnabled) {
            sectionStyleProvider.setSilentSections(listOf(
            sectionStyleProvider.setSilentSections(
                listOf(
                    rankingCoordinator.silentSectioner,
                    rankingCoordinator.minimizedSectioner,
            ))
                )
            )
        } else {
            sectionStyleProvider.setSilentSections(listOf(
            sectionStyleProvider.setSilentSections(
                listOf(
                    conversationCoordinator.peopleSilentSectioner,
                    rankingCoordinator.silentSectioner,
                    rankingCoordinator.minimizedSectioner,
            ))
                )
            )
        }
    }

    /**
     * Sends the pipeline to each coordinator when the pipeline is ready to accept
     * [Pluggable]s, [NotifCollectionListener]s and [NotifLifetimeExtender]s.
     * Sends the pipeline to each coordinator when the pipeline is ready to accept [Pluggable]s,
     * [NotifCollectionListener]s and [NotifLifetimeExtender]s.
     */
    override fun attach(pipeline: NotifPipeline) {
        for (c in mCoreCoordinators) {
@@ -155,7 +165,8 @@ class NotifCoordinatorsImpl @Inject constructor(
     * As part of the NotifPipeline dumpable, dumps the list of coordinators; sections are omitted
     * as they are dumped in the RenderStageManager instead.
     */
    override fun dumpPipeline(d: PipelineDumper) = with(d) {
    override fun dumpPipeline(d: PipelineDumper) =
        with(d) {
            dump("core coordinators", mCoreCoordinators)
            dump("normal coordinators", mCoordinators)
        }
+14 −2
Original line number Diff line number Diff line
@@ -38,6 +38,8 @@ 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.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.util.Compile;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -63,6 +65,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {
    public static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
    private final DelayableExecutor mDelayableExecutor;
    private final HeadsUpManager mHeadsUpManager;
    private final SeenNotificationsInteractor mSeenNotificationsInteractor;
    private final ShadeAnimationInteractor mShadeAnimationInteractor;
    private final StatusBarStateController mStatusBarStateController;
    private final JavaAdapter mJavaAdapter;
@@ -101,6 +104,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {
            HeadsUpManager headsUpManager,
            ShadeAnimationInteractor shadeAnimationInteractor,
            JavaAdapter javaAdapter,
            SeenNotificationsInteractor seenNotificationsInteractor,
            StatusBarStateController statusBarStateController,
            VisibilityLocationProvider visibilityLocationProvider,
            VisualStabilityProvider visualStabilityProvider,
@@ -109,6 +113,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {
        mHeadsUpManager = headsUpManager;
        mShadeAnimationInteractor = shadeAnimationInteractor;
        mJavaAdapter = javaAdapter;
        mSeenNotificationsInteractor = seenNotificationsInteractor;
        mVisibilityLocationProvider = visibilityLocationProvider;
        mVisualStabilityProvider = visualStabilityProvider;
        mWakefulnessLifecycle = wakefulnessLifecycle;
@@ -142,8 +147,15 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {
    private final NotifStabilityManager mNotifStabilityManager =
            new NotifStabilityManager("VisualStabilityCoordinator") {
                private boolean canMoveForHeadsUp(NotificationEntry entry) {
                    return entry != null && mHeadsUpManager.isHeadsUpEntry(entry.getKey())
                            && !mVisibilityLocationProvider.isInVisibleLocation(entry);
                    if (entry == null) {
                        return false;
                    }
                    boolean isTopUnseen = NotificationMinimalismPrototype.V2.isEnabled()
                            && mSeenNotificationsInteractor.isTopUnseenNotification(entry);
                    if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) {
                        return !mVisibilityLocationProvider.isInVisibleLocation(entry);
                    }
                    return false;
                }

                @Override
+3 −0
Original line number Diff line number Diff line
@@ -41,6 +41,9 @@ class ActiveNotificationListRepository @Inject constructor() {

    /** Stats about the list of notifications attached to the shade */
    val notifStats = MutableStateFlow(NotifStats.empty)

    /** The key of the top unseen notification */
    val topUnseenNotificationKey = MutableStateFlow<String?>(null)
}

/** Represents the notification list, comprised of groups and individual notifications. */
Loading