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

Commit f54fc86a authored by Bryce Lee's avatar Bryce Lee
Browse files

Defer future actions until reset it complete.

This changelist makes sure that we don't proceed with future actions
within methods that call reset until the reset is complete. Resets
can be deferred due to in-progress exit animations. This means that
exit logic and then potentially subsequent dream starting logic
might run before the reset logic executes. As a result, components
might be reset during an active state. By deferring resets and then
sequencing follow-up actions, the actions are properly ordered with
their reset requests and each other.

Test: atest DreamOverlayServiceTest
Fixes: 362334941
Flag: EXEMPT bugfix
Change-Id: I4fc722441a323bd818f7b23c84b7e24e6bccce6b
parent 86d29dfd
Loading
Loading
Loading
Loading
+44 −0
Original line number Diff line number Diff line
@@ -92,8 +92,11 @@ import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.firstValue
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verifyZeroInteractions
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
@@ -422,6 +425,47 @@ class DreamOverlayServiceTest : SysuiTestCase() {
        assertThat(mService.shouldShowComplications()).isTrue()
    }

    @Test
    fun testDeferredResetRespondsToAnimationEnd() {
        val client = client

        // Inform the overlay service of dream starting.
        client.startDream(
            mWindowParams,
            mDreamOverlayCallback,
            DREAM_COMPONENT,
            false /*isPreview*/,
            true /*shouldShowComplication*/
        )
        mMainExecutor.runAllReady()

        whenever(mStateController.areExitAnimationsRunning()).thenReturn(true)
        clearInvocations(mStateController, mTouchMonitor)

        // Starting a dream will cause it to end first.
        client.startDream(
            mWindowParams,
            mDreamOverlayCallback,
            DREAM_COMPONENT,
            false /*isPreview*/,
            true /*shouldShowComplication*/
        )

        mMainExecutor.runAllReady()

        verifyZeroInteractions(mTouchMonitor)

        val captor = ArgumentCaptor.forClass(DreamOverlayStateController.Callback::class.java)
        verify(mStateController).addCallback(captor.capture())

        whenever(mStateController.areExitAnimationsRunning()).thenReturn(false)

        captor.firstValue.onStateChanged()

        // Should only be called once since it should be null during the second reset.
        verify(mTouchMonitor).destroy()
    }

    @Test
    fun testLowLightSetByStartDream() {
        val client = client
+121 −56
Original line number Diff line number Diff line
@@ -219,17 +219,123 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
        }
    };

    private final DreamOverlayStateController.Callback mExitAnimationFinishedCallback =
    /**
     * {@link ResetHandler} protects resetting {@link DreamOverlayService} by making sure reset
     * requests are processed before subsequent actions proceed. Requests themselves are also
     * ordered between each other as well to ensure actions are correctly sequenced.
     */
    private final class ResetHandler {
        @FunctionalInterface
        interface Callback {
            void onComplete();
        }

        private record Info(Callback callback, String source) {}

        private final ArrayList<Info> mPendingCallbacks = new ArrayList<>();

        DreamOverlayStateController.Callback mStateCallback =
                new DreamOverlayStateController.Callback() {
                    @Override
                    public void onStateChanged() {
                    if (!mStateController.areExitAnimationsRunning()) {
                        mStateController.removeCallback(mExitAnimationFinishedCallback);
                        resetCurrentDreamOverlayLocked();
                    }
                        process(true);
                    }
                };

        /**
         * Called from places where there is no need to wait for the reset to complete. This still
         * will defer the reset until it is okay to reset and also sequences the request with
         * others.
         */
        public void reset(String source) {
            reset(()-> {}, source);
        }

        /**
         * Invoked to request a reset with a callback that will fire after reset if it is deferred.
         *
         * @return {@code true} if the reset happened immediately, {@code false} if it was deferred
         * and will fire later, invoking the callback.
         */
        public boolean reset(Callback callback, String source) {
            // Always add listener pre-emptively
            if (mPendingCallbacks.isEmpty()) {
                mStateController.addCallback(mStateCallback);
            }

            final Info info = new Info(callback, source);
            mPendingCallbacks.add(info);
            process(false);

            boolean processed = !mPendingCallbacks.contains(info);

            if (!processed) {
                Log.d(TAG, "delayed resetting from: " + source);
            }

            return processed;
        }

        private void resetInternal() {
            // This ensures the container view of the current dream is removed before
            // the controller is potentially reset.
            removeContainerViewFromParentLocked();

            if (mStarted && mWindow != null) {
                try {
                    mWindow.clearContentView();
                    mWindowManager.removeView(mWindow.getDecorView());
                } catch (IllegalArgumentException e) {
                    Log.e(TAG, "Error removing decor view when resetting overlay", e);
                }
            }

            mStateController.setOverlayActive(false);
            mStateController.setLowLightActive(false);
            mStateController.setEntryAnimationsFinished(false);

            if (mDreamOverlayContainerViewController != null) {
                mDreamOverlayContainerViewController.destroy();
                mDreamOverlayContainerViewController = null;
            }

            if (mTouchMonitor != null) {
                mTouchMonitor.destroy();
                mTouchMonitor = null;
            }

            mWindow = null;

            // Always unregister the any set DreamActivity from being blocked from gestures.
            mGestureInteractor.removeGestureBlockedMatcher(DREAM_TYPE_MATCHER,
                    GestureInteractor.Scope.Global);

            mStarted = false;
        }

        private boolean canReset() {
            return !mStateController.areExitAnimationsRunning();
        }

        private void process(boolean fromDelayedCallback) {
            while (canReset() && !mPendingCallbacks.isEmpty()) {
                final Info callbackInfo = mPendingCallbacks.removeFirst();
                resetInternal();
                callbackInfo.callback.onComplete();

                if (fromDelayedCallback) {
                    Log.d(TAG, "reset overlay (delayed) for " + callbackInfo.source);
                }
            }

            if (mPendingCallbacks.isEmpty()) {
                mStateController.removeCallback(mStateCallback);
            }
        }
    }

    private final ResetHandler mResetHandler = new ResetHandler();

    private final DreamOverlayStateController mStateController;

    private final GestureInteractor mGestureInteractor;
@@ -342,10 +448,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ

        mExecutor.execute(() -> {
            setLifecycleStateLocked(Lifecycle.State.DESTROYED);

            resetCurrentDreamOverlayLocked();

            mDestroyed = true;
            mResetHandler.reset("destroying");
        });

        mDispatcher.onServicePreSuperOnDestroy();
@@ -385,7 +489,10 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
            // Reset the current dream overlay before starting a new one. This can happen
            // when two dreams overlap (briefly, for a smoother dream transition) and both
            // dreams are bound to the dream overlay service.
            resetCurrentDreamOverlayLocked();
            if (!mResetHandler.reset(() -> onStartDream(layoutParams),
                    "starting with dream already started")) {
                return;
            }
        }

        mDreamOverlayContainerViewController =
@@ -397,7 +504,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ

        // If we are not able to add the overlay window, reset the overlay.
        if (!addOverlayWindowLocked(layoutParams)) {
            resetCurrentDreamOverlayLocked();
            mResetHandler.reset("couldn't add window while starting");
            return;
        }

@@ -435,7 +542,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ

    @Override
    public void onEndDream() {
        resetCurrentDreamOverlayLocked();
        mResetHandler.reset("ending dream");
    }

    @Override
@@ -566,46 +673,4 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
        Log.w(TAG, "Removing dream overlay container view parent!");
        parentView.removeView(containerView);
    }

    private void resetCurrentDreamOverlayLocked() {
        if (mStateController.areExitAnimationsRunning()) {
            mStateController.addCallback(mExitAnimationFinishedCallback);
            return;
        }

        // This ensures the container view of the current dream is removed before
        // the controller is potentially reset.
        removeContainerViewFromParentLocked();

        if (mStarted && mWindow != null) {
            try {
                mWindow.clearContentView();
                mWindowManager.removeView(mWindow.getDecorView());
            } catch (IllegalArgumentException e) {
                Log.e(TAG, "Error removing decor view when resetting overlay", e);
            }
        }

        mStateController.setOverlayActive(false);
        mStateController.setLowLightActive(false);
        mStateController.setEntryAnimationsFinished(false);

        if (mDreamOverlayContainerViewController != null) {
            mDreamOverlayContainerViewController.destroy();
            mDreamOverlayContainerViewController = null;
        }

        if (mTouchMonitor != null) {
            mTouchMonitor.destroy();
            mTouchMonitor = null;
        }

        mWindow = null;

        // Always unregister the any set DreamActivity from being blocked from gestures.
        mGestureInteractor.removeGestureBlockedMatcher(DREAM_TYPE_MATCHER,
                GestureInteractor.Scope.Global);

        mStarted = false;
    }
}