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

Commit 83476cd3 authored by Steve Elliott's avatar Steve Elliott
Browse files

New pipeline: defer visual updates when collapse

In the old pipeline, StatusBarNotificationPresenter had logic which
deferred changes to the notification shade if the panel was collapsing,
or if an activity was launching.

This CL introduces a change to the new pipeline to replicate the old
behavior. A new API is added to the NotificationStabilityManager,
isPipelineRunAllowed(), that is checked before performing any visual
updates to the notification shade.

isPipelineRunAllowed() is driven by listeners to collapse + activity
launch events emitted from the NotificationPanelViewController. When the
value of isPipelineRunAllowed() would change, the
NotificationStabilityManager is invalidated.

Test: launch activity from notification, verify notification isn't
      removed while activity is launching
Test: atest VisualStabilityCoordinatorTest
Fixes: 205994866

Change-Id: If9921353ee7393a978a84f016ba62856fa13afc4
parent b476f8b5
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -359,6 +359,13 @@ public class ShadeListBuilder implements Dumpable {
    private void buildList() {
        Trace.beginSection("ShadeListBuilder.buildList");
        mPipelineState.requireIsBefore(STATE_BUILD_STARTED);

        if (!mNotifStabilityManager.isPipelineRunAllowed()) {
            mLogger.logPipelineRunSuppressed();
            Trace.endSection();
            return;
        }

        mPipelineState.setState(STATE_BUILD_STARTED);

        // Step 1: Reset notification states
+43 −8
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ 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.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.render.NotifPanelEventSource;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.util.concurrency.DelayableExecutor;

@@ -56,17 +57,23 @@ import javax.inject.Inject;
 */
// TODO(b/204468557): Move to @CoordinatorScope
@SysUISingleton
public class VisualStabilityCoordinator implements Coordinator, Dumpable {
public class VisualStabilityCoordinator implements Coordinator, Dumpable,
        NotifPanelEventSource.Callbacks {
    private final DelayableExecutor mDelayableExecutor;
    private final WakefulnessLifecycle mWakefulnessLifecycle;
    private final StatusBarStateController mStatusBarStateController;
    private final HeadsUpManager mHeadsUpManager;
    private final NotifPanelEventSource mNotifPanelEventSource;
    private final StatusBarStateController mStatusBarStateController;
    private final WakefulnessLifecycle mWakefulnessLifecycle;

    private boolean mScreenOn;
    private boolean mPanelExpanded;
    private boolean mPulsing;
    private boolean mNotifPanelCollapsing;
    private boolean mNotifPanelLaunchingActivity;

    private boolean mPipelineRunAllowed;
    private boolean mReorderingAllowed;
    private boolean mIsSuppressingPipelineRun = false;
    private boolean mIsSuppressingGroupChange = false;
    private final Set<String> mEntriesWithSuppressedSectionChange = new HashSet<>();
    private boolean mIsSuppressingEntryReorder = false;
@@ -81,16 +88,17 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {

    @Inject
    public VisualStabilityCoordinator(
            DelayableExecutor delayableExecutor,
            DumpManager dumpManager,
            HeadsUpManager headsUpManager,
            WakefulnessLifecycle wakefulnessLifecycle,
            NotifPanelEventSource notifPanelEventSource,
            StatusBarStateController statusBarStateController,
            DelayableExecutor delayableExecutor
    ) {
            WakefulnessLifecycle wakefulnessLifecycle) {
        mHeadsUpManager = headsUpManager;
        mWakefulnessLifecycle = wakefulnessLifecycle;
        mStatusBarStateController = statusBarStateController;
        mDelayableExecutor = delayableExecutor;
        mNotifPanelEventSource = notifPanelEventSource;

        dumpManager.registerDumpable(this);
    }
@@ -103,6 +111,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {

        mStatusBarStateController.addCallback(mStatusBarStateControllerListener);
        mPulsing = mStatusBarStateController.isPulsing();
        mNotifPanelEventSource.registerCallbacks(this);

        pipeline.setVisualStabilityManager(mNotifStabilityManager);
    }
@@ -112,11 +121,18 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {
            new NotifStabilityManager("VisualStabilityCoordinator") {
                @Override
                public void onBeginRun() {
                    mIsSuppressingPipelineRun = false;
                    mIsSuppressingGroupChange = false;
                    mEntriesWithSuppressedSectionChange.clear();
                    mIsSuppressingEntryReorder = false;
                }

                @Override
                public boolean isPipelineRunAllowed() {
                    mIsSuppressingPipelineRun |= !mPipelineRunAllowed;
                    return mPipelineRunAllowed;
                }

                @Override
                public boolean isGroupChangeAllowed(@NonNull NotificationEntry entry) {
                    final boolean isGroupChangeAllowedForEntry =
@@ -154,9 +170,12 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {
            };

    private void updateAllowedStates() {
        mPipelineRunAllowed = !isPanelCollapsingOrLaunchingActivity();
        mReorderingAllowed = isReorderingAllowed();
        if (mReorderingAllowed && (mIsSuppressingGroupChange || isSuppressingSectionChange()
                || mIsSuppressingEntryReorder)) {
        if ((mPipelineRunAllowed && mIsSuppressingPipelineRun)
                || (mReorderingAllowed && (mIsSuppressingGroupChange
                        || isSuppressingSectionChange()
                        || mIsSuppressingEntryReorder))) {
            mNotifStabilityManager.invalidateList();
        }
    }
@@ -165,6 +184,10 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {
        return !mEntriesWithSuppressedSectionChange.isEmpty();
    }

    private boolean isPanelCollapsingOrLaunchingActivity() {
        return mNotifPanelCollapsing || mNotifPanelLaunchingActivity;
    }

    private boolean isReorderingAllowed() {
        return (!mScreenOn || !mPanelExpanded) && !mPulsing;
    }
@@ -248,4 +271,16 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {
            pw.println("  " + key);
        }
    }

    @Override
    public void onPanelCollapsingChanged(boolean isCollapsing) {
        mNotifPanelCollapsing = isCollapsing;
        updateAllowedStates();
    }

    @Override
    public void onLaunchingActivityChanged(boolean isLaunchingActivity) {
        mNotifPanelLaunchingActivity = isLaunchingActivity;
        updateAllowedStates();
    }
}
+3 −0
Original line number Diff line number Diff line
@@ -306,6 +306,9 @@ class ShadeListBuilderLogger @Inject constructor(
            }
        }
    }

    fun logPipelineRunSuppressed() =
            buffer.log(TAG, INFO, {}) { "Suppressing pipeline run during animation." }
}

private const val TAG = "ShadeListBuilder"
 No newline at end of file
+11 −0
Original line number Diff line number Diff line
@@ -27,6 +27,16 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry
 */
abstract class NotifStabilityManager protected constructor(name: String) :
    Pluggable<NotifStabilityManager>(name) {

    /**
     * Called prior to running the pipeline to suppress any visual changes. Ex: collapse animation
     * is playing, moving stuff around simultaneously will look janky.
     *
     * Note: this is invoked *before* [onBeginRun], so that implementors can reference state
     * maintained from a previous run.
     */
    abstract fun isPipelineRunAllowed(): Boolean

    /**
     * Called at the beginning of every pipeline run to perform any necessary cleanup from the
     * previous run.
@@ -76,6 +86,7 @@ abstract class NotifStabilityManager protected constructor(name: String) :

/** The default, no-op instance of the stability manager which always allows all changes */
object DefaultNotifStabilityManager : NotifStabilityManager("DefaultNotifStabilityManager") {
    override fun isPipelineRunAllowed(): Boolean = true
    override fun onBeginRun() {}
    override fun isGroupChangeAllowed(entry: NotificationEntry): Boolean = true
    override fun isSectionChangeAllowed(entry: NotificationEntry): Boolean = true
+128 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.render

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.phone.NotificationPanelViewController
import com.android.systemui.statusbar.phone.dagger.StatusBarComponent
import com.android.systemui.statusbar.phone.dagger.StatusBarComponent.StatusBarScope
import com.android.systemui.util.ListenerSet
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet

/** Provides certain notification panel events.  */
interface NotifPanelEventSource {

    /** Registers callbacks to be invoked when notification panel events occur.  */
    fun registerCallbacks(callbacks: Callbacks)

    /** Unregisters callbacks previously registered via [.registerCallbacks]  */
    fun unregisterCallbacks(callbacks: Callbacks)

    /** Callbacks for certain notification panel events. */
    interface Callbacks {

        /** Invoked when the notification panel starts or stops collapsing. */
        fun onPanelCollapsingChanged(isCollapsing: Boolean)

        /**
         * Invoked when the notification panel starts or stops launching an [android.app.Activity].
         */
        fun onLaunchingActivityChanged(isLaunchingActivity: Boolean)
    }
}

@Module
abstract class NotifPanelEventSourceModule {

    @Binds
    @SysUISingleton
    abstract fun bindEventSource(manager: NotifPanelEventSourceManager): NotifPanelEventSource

    @Module
    companion object {
        @JvmStatic
        @Provides
        fun provideManager(): NotifPanelEventSourceManager = NotifPanelEventSourceManagerImpl()
    }
}

@Module
object StatusBarNotifPanelEventSourceModule {
    @JvmStatic
    @Provides
    @IntoSet
    @StatusBarScope
    fun bindStartable(
        manager: NotifPanelEventSourceManager,
        notifPanelController: NotificationPanelViewController
    ): StatusBarComponent.Startable =
            EventSourceStatusBarStartableImpl(manager, notifPanelController)
}

/**
 * Management layer that bridges [SysUiSingleton] and [StatusBarScope]. Necessary because code that
 * wants to listen to [NotifPanelEventSource] lives in [SysUiSingleton], but the events themselves
 * come from [NotificationPanelViewController] in [StatusBarScope].
 */
interface NotifPanelEventSourceManager : NotifPanelEventSource {
    var eventSource: NotifPanelEventSource?
}

private class NotifPanelEventSourceManagerImpl
    : NotifPanelEventSourceManager, NotifPanelEventSource.Callbacks {

    private val callbackSet = ListenerSet<NotifPanelEventSource.Callbacks>()

    override var eventSource: NotifPanelEventSource? = null
        set(value) {
            field?.unregisterCallbacks(this)
            value?.registerCallbacks(this)
            field = value
        }

    override fun registerCallbacks(callbacks: NotifPanelEventSource.Callbacks) {
        callbackSet.addIfAbsent(callbacks)
    }

    override fun unregisterCallbacks(callbacks: NotifPanelEventSource.Callbacks) {
        callbackSet.remove(callbacks)
    }

    override fun onPanelCollapsingChanged(isCollapsing: Boolean) {
        callbackSet.forEach { it.onPanelCollapsingChanged(isCollapsing) }
    }

    override fun onLaunchingActivityChanged(isLaunchingActivity: Boolean) {
        callbackSet.forEach { it.onLaunchingActivityChanged(isLaunchingActivity) }
    }
}

private class EventSourceStatusBarStartableImpl(
    private val manager: NotifPanelEventSourceManager,
    private val notifPanelController: NotificationPanelViewController
) : StatusBarComponent.Startable {

    override fun start() {
        manager.eventSource = notifPanelController
    }

    override fun stop() {
        manager.eventSource = null
    }
}
Loading