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

Commit 0c217d3b authored by Bryce Lee's avatar Bryce Lee Committed by Android (Google) Code Review
Browse files

Merge "Defer future actions until reset it complete." into main

parents bec85add f54fc86a
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;
    }
}