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

Commit a37504d3 authored by Steve Elliott's avatar Steve Elliott Committed by Android (Google) Code Review
Browse files

Merge "Asynchronous notif pipeline evaluation"

parents 6eba9508 a06ea9df
Loading
Loading
Loading
Loading
+140 −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

import android.view.Choreographer
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.util.ListenerSet
import com.android.systemui.util.concurrency.DelayableExecutor
import dagger.Module
import dagger.Provides

/**
 * Choreographs evaluation resulting from multiple asynchronous sources. Specifically, it exposes
 * [schedule], and [addOnEvalListener]; the former will "schedule" an asynchronous invocation of the
 * latter. Multiple invocations of [schedule] before any added listeners are invoked have no effect.
 */
interface NotifPipelineChoreographer {
    /**
     * Schedules all listeners registered with [addOnEvalListener] to be asynchronously executed at
     * some point in the future. The exact timing is up to the implementation.
     */
    fun schedule()

    /** Cancels a pending evaluation triggered by any recent calls to [schedule]. */
    fun cancel()

    /** Adds a listener [Runnable] that will be invoked when the scheduled evaluation occurs. */
    fun addOnEvalListener(onEvalListener: Runnable)

    /** Removes a listener previously registered with [addOnEvalListener]. */
    fun removeOnEvalListener(onEvalListener: Runnable)
}

@Module
object NotifPipelineChoreographerModule {
    @Provides
    @JvmStatic
    @SysUISingleton
    fun provideChoreographer(
        choreographer: Choreographer,
        @Main mainExecutor: DelayableExecutor
    ): NotifPipelineChoreographer = NotifPipelineChoreographerImpl(choreographer, mainExecutor)
}

private const val TIMEOUT_MS: Long = 100

private class NotifPipelineChoreographerImpl(
    private val viewChoreographer: Choreographer,
    private val executor: DelayableExecutor
) : NotifPipelineChoreographer {

    private val listeners = ListenerSet<Runnable>()
    private var timeoutSubscription: Runnable? = null
    private var isScheduled = false

    private val frameCallback = Choreographer.FrameCallback {
        if (isScheduled) {
            isScheduled = false
            timeoutSubscription?.run()
            listeners.forEach { it.run() }
        }
    }

    override fun schedule() {
        if (isScheduled) return
        isScheduled = true
        viewChoreographer.postFrameCallback(frameCallback)
        if (!isScheduled) {
            // Guard against synchronous evaluation of the frame callback.
            return
        }
        timeoutSubscription = executor.executeDelayed(::onTimeout, TIMEOUT_MS)
    }

    override fun cancel() {
        if (!isScheduled) return
        timeoutSubscription?.run()
        viewChoreographer.removeFrameCallback(frameCallback)
    }

    override fun addOnEvalListener(onEvalListener: Runnable) {
        listeners.addIfAbsent(onEvalListener)
    }

    override fun removeOnEvalListener(onEvalListener: Runnable) {
        listeners.remove(onEvalListener)
    }

    private fun onTimeout() {
        if (isScheduled) {
            isScheduled = false
            viewChoreographer.removeFrameCallback(frameCallback)
            listeners.forEach { it.run() }
        }
    }
}

class FakeNotifPipelineChoreographer : NotifPipelineChoreographer {

    var isScheduled = false
    val listeners = ListenerSet<Runnable>()

    fun runIfScheduled() {
        if (isScheduled) {
            isScheduled = false
            listeners.forEach { it.run() }
        }
    }

    override fun schedule() {
        isScheduled = true
    }

    override fun cancel() {
        isScheduled = false
    }

    override fun addOnEvalListener(onEvalListener: Runnable) {
        listeners.addIfAbsent(onEvalListener)
    }

    override fun removeOnEvalListener(onEvalListener: Runnable) {
        listeners.remove(onEvalListener)
    }
}
+9 −5
Original line number Diff line number Diff line
@@ -122,20 +122,23 @@ public class ShadeListBuilder implements Dumpable {

    private List<ListEntry> mReadOnlyNotifList = Collections.unmodifiableList(mNotifList);
    private List<ListEntry> mReadOnlyNewNotifList = Collections.unmodifiableList(mNewNotifList);
    private final NotifPipelineChoreographer mChoreographer;

    @Inject
    public ShadeListBuilder(
            SystemClock systemClock,
            DumpManager dumpManager,
            NotifPipelineChoreographer pipelineChoreographer,
            NotifPipelineFlags flags,
            NotificationInteractionTracker interactionTracker,
            ShadeListBuilderLogger logger,
            DumpManager dumpManager,
            NotificationInteractionTracker interactionTracker
            SystemClock systemClock
    ) {
        Assert.isMainThread();
        mSystemClock = systemClock;
        mLogger = logger;
        mAlwaysLogList = flags.isDevLoggingEnabled();
        mInteractionTracker = interactionTracker;
        mChoreographer = pipelineChoreographer;
        dumpManager.registerDumpable(TAG, this);

        setSectioners(Collections.emptyList());
@@ -148,6 +151,7 @@ public class ShadeListBuilder implements Dumpable {
    public void attach(NotifCollection collection) {
        Assert.isMainThread();
        collection.setBuildListener(mReadyForBuildListener);
        mChoreographer.addOnEvalListener(this::buildList);
    }

    /**
@@ -290,7 +294,7 @@ public class ShadeListBuilder implements Dumpable {

                    mLogger.logOnBuildList();
                    mAllEntries = entries;
                    buildList();
                    mChoreographer.schedule();
                }
            };

@@ -1281,7 +1285,7 @@ public class ShadeListBuilder implements Dumpable {
    private void rebuildListIfBefore(@PipelineState.StateName int state) {
        mPipelineState.requireIsBefore(state);
        if (mPipelineState.is(STATE_IDLE)) {
            buildList();
            mChoreographer.schedule();
        }
    }

+2 −0
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl;
import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore;
import com.android.systemui.statusbar.notification.collection.NotifLiveDataStoreImpl;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotifPipelineChoreographerModule;
import com.android.systemui.statusbar.notification.collection.coordinator.ShadeEventCoordinator;
import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator;
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorsModule;
@@ -103,6 +104,7 @@ import dagger.Provides;
 */
@Module(includes = {
        CoordinatorsModule.class,
        NotifPipelineChoreographerModule.class,
        NotifPanelEventSourceModule.class,
        NotificationSectionHeadersModule.class,
})
+100 −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

import android.testing.AndroidTestingRunner
import android.view.Choreographer
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever

@SmallTest
@RunWith(AndroidTestingRunner::class)
class NotifPipelineChoreographerTest : SysuiTestCase() {

    val viewChoreographer: Choreographer = mock()
    val timeoueSubscription: Runnable = mock()
    val executor: DelayableExecutor = mock<DelayableExecutor>().also {
        whenever(it.executeDelayed(any(), anyLong())).thenReturn(timeoueSubscription)
    }

    val pipelineChoreographer: NotifPipelineChoreographer = NotifPipelineChoreographerModule
            .provideChoreographer(viewChoreographer, executor)

    @Test
    fun scheduleThenEvalFrameCallback() {
        // GIVEN a registered eval listener and scheduled choreographer
        var hasEvaluated = false
        pipelineChoreographer.addOnEvalListener {
            hasEvaluated = true
        }
        pipelineChoreographer.schedule()
        val frameCallback: Choreographer.FrameCallback = withArgCaptor {
            verify(viewChoreographer).postFrameCallback(capture())
        }
        // WHEN the choreographer would invoke its callback
        frameCallback.doFrame(0)
        // THEN the choreographer would evaluate, and the timeoutSubscription would have been
        // cancelled
        assertTrue(hasEvaluated)
        verify(timeoueSubscription).run()
    }

    @Test
    fun scheduleThenEvalTimeoutCallback() {
        // GIVEN a registered eval listener and scheduled choreographer
        var hasEvaluated = false
        pipelineChoreographer.addOnEvalListener {
            hasEvaluated = true
        }
        pipelineChoreographer.schedule()
        val frameCallback: Choreographer.FrameCallback = withArgCaptor {
            verify(viewChoreographer).postFrameCallback(capture())
        }
        val runnable: Runnable = withArgCaptor {
            verify(executor).executeDelayed(capture(), anyLong())
        }
        // WHEN the executor would invoke its callback (indicating a timeout)
        runnable.run()
        // THEN the choreographer would evaluate, and the FrameCallback would have been unregistered
        assertTrue(hasEvaluated)
        verify(viewChoreographer).removeFrameCallback(frameCallback)
    }

    @Test
    fun scheduleThenCancel() {
        // GIVEN a scheduled choreographer
        pipelineChoreographer.schedule()
        val frameCallback: Choreographer.FrameCallback = withArgCaptor {
            verify(viewChoreographer).postFrameCallback(capture())
        }
        // WHEN the scheduled run is cancelled
        pipelineChoreographer.cancel()
        // THEN both the FrameCallback is unregistered and the timeout subscription is cancelled.
        verify(viewChoreographer).removeFrameCallback(frameCallback)
        verify(timeoueSubscription).run()
    }
}
 No newline at end of file
+46 −3
Original line number Diff line number Diff line
@@ -111,6 +111,8 @@ public class ShadeListBuilderTest extends SysuiTestCase {

    @Captor private ArgumentCaptor<CollectionReadyForBuildListener> mBuildListenerCaptor;

    private final FakeNotifPipelineChoreographer mPipelineChoreographer =
            new FakeNotifPipelineChoreographer();
    private CollectionReadyForBuildListener mReadyForBuildListener;
    private List<NotificationEntryBuilder> mPendingSet = new ArrayList<>();
    private List<NotificationEntry> mEntrySet = new ArrayList<>();
@@ -127,11 +129,12 @@ public class ShadeListBuilderTest extends SysuiTestCase {
        allowTestableLooperAsMainThread();

        mListBuilder = new ShadeListBuilder(
                mSystemClock,
                mDumpManager,
                mPipelineChoreographer,
                mNotifPipelineFlags,
                mInteractionTracker,
                mLogger,
                mDumpManager,
                mInteractionTracker
                mSystemClock
        );
        mListBuilder.setOnRenderListListener(mOnRenderListListener);

@@ -567,6 +570,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {

        // WHEN the pipeline is kicked off
        mReadyForBuildListener.onBuildList(singletonList(entry));
        mPipelineChoreographer.runIfScheduled();

        // THEN the entry's initialization time is reset
        assertFalse(entry.hasFinishedInitialization());
@@ -1024,26 +1028,38 @@ public class ShadeListBuilderTest extends SysuiTestCase {

        clearInvocations(mOnRenderListListener);
        packageFilter.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());

        clearInvocations(mOnRenderListListener);
        idPromoter.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());

        clearInvocations(mOnRenderListListener);
        section.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());

        clearInvocations(mOnRenderListListener);
        hypeComparator.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());

        clearInvocations(mOnRenderListListener);
        sectionComparator.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());

        clearInvocations(mOnRenderListListener);
        preRenderInvalidator.invalidateList();
        assertTrue(mPipelineChoreographer.isScheduled());
        mPipelineChoreographer.runIfScheduled();
        verify(mOnRenderListListener).onRenderList(anyList());
    }

@@ -1515,6 +1531,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
        // WHEN visual stability manager allows group changes again
        mStabilityManager.setAllowGroupChanges(true);
        mStabilityManager.invalidateList();
        mPipelineChoreographer.runIfScheduled();

        // THEN entries are grouped
        verifyBuiltList(
@@ -1553,6 +1570,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
        // WHEN section changes are allowed again
        mStabilityManager.setAllowSectionChanges(true);
        mStabilityManager.invalidateList();
        mPipelineChoreographer.runIfScheduled();

        // THEN the section updates
        assertEquals(newSectioner, mEntrySet.get(0).getSection().getSectioner());
@@ -1772,6 +1790,30 @@ public class ShadeListBuilderTest extends SysuiTestCase {
        verifyBuiltList();
    }

    @Test
    public void testMultipleInvalidationsCoalesce() {
        // GIVEN a PreGroupFilter and a FinalizeFilter
        NotifFilter filter1 = new PackageFilter(PACKAGE_5);
        NotifFilter filter2 = new PackageFilter(PACKAGE_0);
        mListBuilder.addPreGroupFilter(filter1);
        mListBuilder.addFinalizeFilter(filter2);

        // WHEN both filters invalidate
        filter1.invalidateList();
        filter2.invalidateList();

        // THEN the pipeline choreographer is scheduled to evaluate, AND the pipeline hasn't
        // actually run.
        assertTrue(mPipelineChoreographer.isScheduled());
        verify(mOnRenderListListener, never()).onRenderList(anyList());

        // WHEN the pipeline choreographer actually runs
        mPipelineChoreographer.runIfScheduled();

        // THEN the pipeline runs
        verify(mOnRenderListListener).onRenderList(anyList());
    }

    @Test
    public void testIsSorted() {
        Comparator<Integer> intCmp = Integer::compare;
@@ -1914,6 +1956,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
        }

        mReadyForBuildListener.onBuildList(mEntrySet);
        mPipelineChoreographer.runIfScheduled();
    }

    private void verifyBuiltList(ExpectedEntry ...expectedEntries) {