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

Commit 5903ebdc authored by Nicolo' Mazzucato's avatar Nicolo' Mazzucato
Browse files

Drive Launcher unfold animation from System UI

Before this cl, both Launcher and System UI processes were registering for hinge angle and device state (e.g. folded/unfolded) change events to calculate the current unfold animation progress. In some cases, launcher ui thread was busy, delaying the progress calculation from the hinge angle value (that was received ~at the same time by both processes). This resulted in launcher and sysui unfold animation not being synchronized in some cases.

With this cl, System UI process uses OverviewProxyService to send the unfold events to Launcher. In this way, both process always have the exact same progress (+- 1 frame)

This is currently guarded by a launcher flag, by default with the new behaviour, to allow devs to compare the experience and easily debug potential regressions.

Bug: 268490854
Test: Analysed perfetto trace + RemoteUnfoldTransitionProgressProviderTest + manual
Change-Id: Icdf3932644a545f4dbe077c8284911bf99f3d81c
parent a3f6566a
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@ public class QuickStepContract {
    public static final String KEY_EXTRA_SYSUI_PROXY = "extra_sysui_proxy";
    public static final String KEY_EXTRA_WINDOW_CORNER_RADIUS = "extra_window_corner_radius";
    public static final String KEY_EXTRA_SUPPORTS_WINDOW_CORNERS = "extra_supports_window_corners";
    public static final String KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER = "extra_unfold_animation";
    // See ISysuiUnlockAnimationController.aidl
    public static final String KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER = "unlock_animation";

+18 −0
Original line number Diff line number Diff line
@@ -35,6 +35,8 @@ import com.android.systemui.unfold.FoldStateLogger;
import com.android.systemui.unfold.FoldStateLoggingProvider;
import com.android.systemui.unfold.SysUIUnfoldComponent;
import com.android.systemui.unfold.UnfoldLatencyTracker;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder;
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider;
import com.android.wm.shell.TaskViewFactory;
import com.android.wm.shell.back.BackAnimation;
@@ -137,6 +139,10 @@ public interface SysUIComponent {
        getUnfoldLatencyTracker().init();
        getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init);
        getFoldStateLogger().ifPresent(FoldStateLogger::init);
        getUnfoldTransitionProgressProvider().ifPresent((progressProvider) ->
                getUnfoldTransitionProgressForwarder().ifPresent((forwarder) ->
                        progressProvider.addCallback(forwarder)
                ));
    }

    /**
@@ -163,6 +169,18 @@ public interface SysUIComponent {
    @SysUISingleton
    UnfoldLatencyTracker getUnfoldLatencyTracker();

    /**
     * Creates a UnfoldTransitionProgressProvider.
     */
    @SysUISingleton
    Optional<UnfoldTransitionProgressProvider> getUnfoldTransitionProgressProvider();

    /**
     * Creates a UnfoldTransitionProgressForwarder.
     */
    @SysUISingleton
    Optional<UnfoldTransitionProgressForwarder> getUnfoldTransitionProgressForwarder();

    /**
     * Creates a FoldStateLoggingProvider.
     */
+11 −1
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SUPPORTS_WINDOW_CORNERS;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_WINDOW_CORNER_RADIUS;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
@@ -99,6 +100,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
import com.android.systemui.statusbar.policy.CallbackController;
import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder;
import com.android.wm.shell.sysui.ShellInterface;

import java.io.PrintWriter;
@@ -144,6 +146,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
    private final CommandQueue mCommandQueue;
    private final UserTracker mUserTracker;
    private final KeyguardUnlockAnimationController mSysuiUnlockAnimationController;
    private final Optional<UnfoldTransitionProgressForwarder> mUnfoldTransitionProgressForwarder;
    private final UiEventLogger mUiEventLogger;
    private final DisplayTracker mDisplayTracker;

@@ -415,6 +418,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
            params.putBoolean(KEY_EXTRA_SUPPORTS_WINDOW_CORNERS, mSupportsRoundedCornersOnWindows);
            params.putBinder(KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER,
                    mSysuiUnlockAnimationController.asBinder());
            mUnfoldTransitionProgressForwarder.ifPresent(
                    unfoldProgressForwarder -> params.putBinder(
                            KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER,
                            unfoldProgressForwarder.asBinder()));
            // Add all the interfaces exposed by the shell
            mShellInterface.createExternalInterfaces(params);

@@ -512,7 +519,9 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
            DisplayTracker displayTracker,
            KeyguardUnlockAnimationController sysuiUnlockAnimationController,
            AssistUtils assistUtils,
            DumpManager dumpManager) {
            DumpManager dumpManager,
            Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder
    ) {
        // b/241601880: This component shouldn't be running for a non-primary user
        if (!Process.myUserHandle().equals(UserHandle.SYSTEM)) {
            Log.e(TAG_OPS, "Unexpected initialization for non-primary user", new Throwable());
@@ -538,6 +547,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
        mSysUiState.addCallback(this::notifySystemUiStateFlags);
        mUiEventLogger = uiEventLogger;
        mDisplayTracker = displayTracker;
        mUnfoldTransitionProgressForwarder = unfoldTransitionProgressForwarder;

        dumpManager.registerDumpable(getClass().getSimpleName(), this);

+7 −109
Original line number Diff line number Diff line
@@ -20,17 +20,13 @@ import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING
import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING
import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
import com.android.systemui.unfold.util.TestFoldStateProvider
import com.android.systemui.util.leak.ReferenceTestUtils.waitForCondition
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -45,9 +41,7 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {

    @Before
    fun setUp() {
        progressProvider = PhysicsBasedUnfoldTransitionProgressProvider(
            foldStateProvider
        )
        progressProvider = PhysicsBasedUnfoldTransitionProgressProvider(foldStateProvider)
        progressProvider.addCallback(listener)
    }

@@ -79,9 +73,7 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) },
        )

        with(listener.ensureTransitionFinished()) {
            assertHasSingleFinishingEvent()
        }
        with(listener.ensureTransitionFinished()) { assertHasSingleFinishingEvent() }
    }

    @Test
@@ -150,106 +142,12 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) },
        )

        with(listener.ensureTransitionFinished()) {
            assertHasFoldAnimationAtTheEnd()
        }
    }

    private class TestUnfoldProgressListener : TransitionProgressListener {

        private val recordings: MutableList<UnfoldTransitionRecording> = arrayListOf()
        private var currentRecording: UnfoldTransitionRecording? = null

        override fun onTransitionStarted() {
            assertWithMessage("Trying to start a transition when it is already in progress")
                .that(currentRecording).isNull()

            currentRecording = UnfoldTransitionRecording()
        }

        override fun onTransitionProgress(progress: Float) {
            assertWithMessage("Received transition progress event when it's not started")
                .that(currentRecording).isNotNull()
            currentRecording!!.addProgress(progress)
        }

        override fun onTransitionFinishing() {
            assertWithMessage("Received transition finishing event when it's not started")
                    .that(currentRecording).isNotNull()
            currentRecording!!.onFinishing()
        }

        override fun onTransitionFinished() {
            assertWithMessage("Received transition finish event when it's not started")
                .that(currentRecording).isNotNull()
            recordings += currentRecording!!
            currentRecording = null
        }

        fun ensureTransitionFinished(): UnfoldTransitionRecording {
            waitForCondition { recordings.size == 1 }
            return recordings.first()
        }

        class UnfoldTransitionRecording {
            private val progressHistory: MutableList<Float> = arrayListOf()
            private var finishingInvocations: Int = 0

            fun addProgress(progress: Float) {
                assertThat(progress).isAtMost(1.0f)
                assertThat(progress).isAtLeast(0.0f)

                progressHistory += progress
            }

            fun onFinishing() {
                finishingInvocations++
            }

            fun assertIncreasingProgress() {
                assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
                assertThat(progressHistory).isInOrder()
            }

            fun assertDecreasingProgress() {
                assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
                assertThat(progressHistory).isInOrder(Comparator.reverseOrder<Float>())
            }

            fun assertFinishedWithUnfold() {
                assertThat(progressHistory).isNotEmpty()
                assertThat(progressHistory.last()).isEqualTo(1.0f)
            }

            fun assertFinishedWithFold() {
                assertThat(progressHistory).isNotEmpty()
                assertThat(progressHistory.last()).isEqualTo(0.0f)
            }

            fun assertHasFoldAnimationAtTheEnd() {
                // Check that there are at least a few decreasing events at the end
                assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
                assertThat(progressHistory.takeLast(MIN_ANIMATION_EVENTS))
                    .isInOrder(Comparator.reverseOrder<Float>())
                assertThat(progressHistory.last()).isEqualTo(0.0f)
            }

            fun assertHasSingleFinishingEvent() {
                assertWithMessage("onTransitionFinishing callback should be invoked exactly " +
                        "one time").that(finishingInvocations).isEqualTo(1)
            }
        }

        private companion object {
            private const val MIN_ANIMATION_EVENTS = 5
        }
        with(listener.ensureTransitionFinished()) { assertHasFoldAnimationAtTheEnd() }
    }

    private fun runOnMainThreadWithInterval(vararg blocks: () -> Unit, intervalMillis: Long = 60) {
        blocks.forEach {
            InstrumentationRegistry.getInstrumentation().runOnMainSync {
                it()
            }
            InstrumentationRegistry.getInstrumentation().runOnMainSync { it() }
            Thread.sleep(intervalMillis)
        }
    }
+72 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.unfold.progress

import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

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

    private val progressProvider = RemoteUnfoldTransitionReceiver { it.run() }
    private val listener = TestUnfoldProgressListener()

    @Before
    fun setUp() {
        progressProvider.addCallback(listener)
    }

    @Test
    fun onTransitionStarted_propagated() {
        progressProvider.onTransitionStarted()

        listener.assertStarted()
    }

    @Test
    fun onTransitionProgress_propagated() {
        progressProvider.onTransitionStarted()

        progressProvider.onTransitionProgress(0.5f)

        listener.assertLastProgress(0.5f)
    }

    @Test
    fun onTransitionEnded_propagated() {
        progressProvider.onTransitionStarted()
        progressProvider.onTransitionProgress(0.5f)

        progressProvider.onTransitionFinished()

        listener.ensureTransitionFinished()
    }

    @Test
    fun onTransitionStarted_afterCallbackRemoved_notPropagated() {
        progressProvider.removeCallback(listener)

        progressProvider.onTransitionStarted()

        listener.assertNotStarted()
    }
}
Loading