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

Commit a7b0ff88 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add SystemModalsTransitionHandler to deal with system modal transitions" into main

parents f68d1721 bbef2359
Loading
Loading
Loading
Loading
+24 −1
Original line number Original line Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.wm.shell.dagger;
package com.android.wm.shell.dagger;


import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS;
import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS;
import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY;
import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT;
import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT;
import static android.window.DesktopModeFlags.ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS;
import static android.window.DesktopModeFlags.ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS;


@@ -94,6 +95,7 @@ import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator;
import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.desktopmode.compatui.SystemModalsTransitionHandler;
import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter;
import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter;
import com.android.wm.shell.desktopmode.education.AppToWebEducationController;
import com.android.wm.shell.desktopmode.education.AppToWebEducationController;
@@ -906,6 +908,26 @@ public abstract class WMShellModule {
                focusTransitionObserver, desktopModeEventLogger));
                focusTransitionObserver, desktopModeEventLogger));
    }
    }


    @WMSingleton
    @Provides
    static Optional<SystemModalsTransitionHandler> provideSystemModalsTransitionHandler(
            Context context,
            @ShellMainThread ShellExecutor mainExecutor,
            @ShellAnimationThread ShellExecutor animExecutor,
            ShellInit shellInit,
            Transitions transitions,
            @DynamicOverride DesktopRepository desktopRepository) {
        if (!DesktopModeStatus.canEnterDesktopMode(context)
                || !ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue()
                || !Flags.enableDesktopSystemDialogsTransitions()) {
            return Optional.empty();
        }
        return Optional.of(
                new SystemModalsTransitionHandler(
                        context, mainExecutor, animExecutor, shellInit, transitions,
                        desktopRepository));
    }

    @WMSingleton
    @WMSingleton
    @Provides
    @Provides
    static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler(
    static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler(
@@ -1262,7 +1284,8 @@ public abstract class WMShellModule {
            @NonNull LetterboxCommandHandler letterboxCommandHandler,
            @NonNull LetterboxCommandHandler letterboxCommandHandler,
            Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional,
            Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional,
            Optional<DesktopDisplayEventHandler> desktopDisplayEventHandler,
            Optional<DesktopDisplayEventHandler> desktopDisplayEventHandler,
            Optional<DesktopModeKeyGestureHandler> desktopModeKeyGestureHandler) {
            Optional<DesktopModeKeyGestureHandler> desktopModeKeyGestureHandler,
            Optional<SystemModalsTransitionHandler> systemModalsTransitionHandler) {
        return new Object();
        return new Object();
    }
    }


+174 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2024 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.wm.shell.desktopmode.compatui

import android.animation.ValueAnimator
import android.content.Context
import android.os.IBinder
import android.view.Display.DEFAULT_DISPLAY
import android.view.SurfaceControl
import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.core.animation.addListener
import com.android.app.animation.Interpolators
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing
import com.android.wm.shell.desktopmode.DesktopRepository
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.TransitionUtil.isClosingMode
import com.android.wm.shell.shared.TransitionUtil.isClosingType
import com.android.wm.shell.shared.TransitionUtil.isOpeningMode
import com.android.wm.shell.shared.TransitionUtil.isOpeningType
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionHandler

/** Handles transitions related to system modals, e.g. launch and close transitions. */
class SystemModalsTransitionHandler(
    private val context: Context,
    private val mainExecutor: ShellExecutor,
    private val animExecutor: ShellExecutor,
    private val shellInit: ShellInit,
    private val transitions: Transitions,
    private val desktopRepository: DesktopRepository,
) : TransitionHandler {

    private val showingSystemModalsIds = mutableSetOf<Int>()

    init {
        shellInit.addInitCallback({ transitions.addHandler(this) }, this)
    }

    override fun startAnimation(
        transition: IBinder,
        info: TransitionInfo,
        startTransaction: SurfaceControl.Transaction,
        finishTransaction: SurfaceControl.Transaction,
        finishCallback: Transitions.TransitionFinishCallback,
    ): Boolean {
        if (!isDesktopModeShowing(DEFAULT_DISPLAY)) return false
        if (isOpeningType(info.type)) {
            val launchChange = getLaunchingSystemModal(info) ?: return false
            val taskInfo = launchChange.taskInfo
            requireNotNull(taskInfo)
            logV("Animating system modal launch: taskId=%d", taskInfo.taskId)
            showingSystemModalsIds.add(taskInfo.taskId)
            animateSystemModal(
                launchChange.leash,
                startTransaction,
                finishTransaction,
                finishCallback,
                /* toShow= */ true,
            )
            return true
        }
        if (isClosingType(info.type)) {
            val closeChange = getClosingSystemModal(info) ?: return false
            val taskInfo = closeChange.taskInfo
            requireNotNull(taskInfo)
            logV("Animating system modal close: taskId=%d", taskInfo.taskId)
            showingSystemModalsIds.remove(taskInfo.taskId)
            animateSystemModal(
                closeChange.leash,
                startTransaction,
                finishTransaction,
                finishCallback,
                /* toShow= */ false,
            )
            return true
        }
        return false
    }

    private fun animateSystemModal(
        leash: SurfaceControl,
        startTransaction: SurfaceControl.Transaction,
        finishTransaction: SurfaceControl.Transaction,
        finishCallback: Transitions.TransitionFinishCallback,
        toShow: Boolean, // Whether to show or to hide the system modal
    ) {
        val startAlpha = if (toShow) 0f else 1f
        val endAlpha = if (toShow) 1f else 0f
        val animator =
            createAlphaAnimator(SurfaceControl.Transaction(), leash, startAlpha, endAlpha)
        animator.addListener(
            onEnd = { _ ->
                mainExecutor.execute { finishCallback.onTransitionFinished(/* wct= */ null) }
            }
        )
        if (toShow) {
            finishTransaction.show(leash)
        } else {
            finishTransaction.hide(leash)
        }
        startTransaction.setAlpha(leash, startAlpha)
        startTransaction.apply()
        animExecutor.execute { animator.start() }
    }

    private fun getLaunchingSystemModal(info: TransitionInfo): TransitionInfo.Change? =
        info.changes.find { change ->
            if (!isOpeningMode(change.mode)) {
                return@find false
            }
            val taskInfo = change.taskInfo ?: return@find false
            return@find isTopActivityExemptFromDesktopWindowing(context, taskInfo)
        }

    private fun getClosingSystemModal(info: TransitionInfo): TransitionInfo.Change? =
        info.changes.find { change ->
            if (!isClosingMode(change.mode)) {
                return@find false
            }
            val taskInfo = change.taskInfo ?: return@find false
            return@find isTopActivityExemptFromDesktopWindowing(context, taskInfo) ||
                showingSystemModalsIds.contains(taskInfo.taskId)
        }

    private fun createAlphaAnimator(
        transaction: SurfaceControl.Transaction,
        leash: SurfaceControl,
        startVal: Float,
        endVal: Float,
    ): ValueAnimator =
        ValueAnimator.ofFloat(startVal, endVal).apply {
            duration = LAUNCH_ANIM_ALPHA_DURATION_MS
            interpolator = Interpolators.LINEAR
            addUpdateListener { animation ->
                transaction.setAlpha(leash, animation.animatedValue as Float).apply()
            }
        }

    private fun isDesktopModeShowing(displayId: Int): Boolean =
        desktopRepository.getVisibleTaskCount(displayId) > 0

    override fun handleRequest(
        transition: IBinder,
        request: TransitionRequestInfo,
    ): WindowContainerTransaction? = null

    private fun logV(msg: String, vararg arguments: Any?) {
        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
    }

    companion object {
        private const val TAG = "SystemModalsTransitionHandler"
        private const val LAUNCH_ANIM_ALPHA_DURATION_MS = 150L
    }
}
+19 −3
Original line number Original line Diff line number Diff line
@@ -46,17 +46,23 @@ class DesktopTestHelpers {
                    .build()
                    .build()
        }
        }


        /** Create a task that has windowing mode set to [WINDOWING_MODE_FULLSCREEN] */
        /** Create a task builder that has windowing mode set to [WINDOWING_MODE_FULLSCREEN] */
        @JvmStatic
        @JvmStatic
        @JvmOverloads
        @JvmOverloads
        fun createFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
        fun createFullscreenTaskBuilder(displayId: Int = DEFAULT_DISPLAY): TestRunningTaskInfoBuilder {
            return TestRunningTaskInfoBuilder()
            return TestRunningTaskInfoBuilder()
                .setDisplayId(displayId)
                .setDisplayId(displayId)
                .setToken(MockToken().token())
                .setToken(MockToken().token())
                .setActivityType(ACTIVITY_TYPE_STANDARD)
                .setActivityType(ACTIVITY_TYPE_STANDARD)
                .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
                .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
                .setLastActiveTime(100)
                .setLastActiveTime(100)
                .build()
        }

        /** Create a task that has windowing mode set to [WINDOWING_MODE_FULLSCREEN] */
        @JvmStatic
        @JvmOverloads
        fun createFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
            return createFullscreenTaskBuilder(displayId).build()
        }
        }


        /** Create a task that has windowing mode set to [WINDOWING_MODE_MULTI_WINDOW] */
        /** Create a task that has windowing mode set to [WINDOWING_MODE_MULTI_WINDOW] */
@@ -84,5 +90,15 @@ class DesktopTestHelpers {
                    .setLastActiveTime(100)
                    .setLastActiveTime(100)
                    .build()
                    .build()
        }
        }

        /** Create a new System Modal task, i.e. a task with a single transparent activity. */
        @JvmStatic
        @JvmOverloads
        fun createSystemModalTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
            return createFullscreenTaskBuilder(displayId)
                .setTopActivityTransparent(true)
                .setNumActivities(1)
                .build()
        }
    }
    }
}
}
 No newline at end of file
+157 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2024 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.wm.shell.desktopmode.compatui

import android.os.Binder
import android.testing.AndroidTestingRunner
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.desktopmode.DesktopRepository
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTaskBuilder
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSystemModalTask
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.TransitionInfoBuilder
import com.android.wm.shell.transition.Transitions
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidTestingRunner::class)
class SystemModalsTransitionHandlerTest : ShellTestCase() {
    private val mainExecutor = mock<ShellExecutor>()
    private val animExecutor = mock<ShellExecutor>()
    private val shellInit = mock<ShellInit>()
    private val transitions = mock<Transitions>()
    private val desktopRepository = mock<DesktopRepository>()
    private val startT = mock<SurfaceControl.Transaction>()
    private val finishT = mock<SurfaceControl.Transaction>()

    private lateinit var transitionHandler: SystemModalsTransitionHandler

    @Before
    fun setUp() {
        // Simulate having one Desktop task so that we see Desktop Mode as active
        whenever(desktopRepository.getVisibleTaskCount(anyInt())).thenReturn(1)
        transitionHandler = createTransitionHandler()
    }

    private fun createTransitionHandler() =
        SystemModalsTransitionHandler(
            context,
            mainExecutor,
            animExecutor,
            shellInit,
            transitions,
            desktopRepository,
        )

    @Test
    fun instantiate_addsInitCallback() {
        verify(shellInit).addInitCallback(any(), any<SystemModalsTransitionHandler>())
    }

    @Test
    fun startAnimation_desktopNotActive_doesNotAnimate() {
        whenever(desktopRepository.getVisibleTaskCount(anyInt())).thenReturn(1)
        val info =
            TransitionInfoBuilder(TRANSIT_OPEN)
                .addChange(TRANSIT_OPEN, createSystemModalTask())
                .build()

        assertThat(transitionHandler.startAnimation(Binder(), info, startT, finishT) {}).isTrue()
    }

    @Test
    fun startAnimation_launchingSystemModal_animates() {
        val info =
            TransitionInfoBuilder(TRANSIT_OPEN)
                .addChange(TRANSIT_OPEN, createSystemModalTask())
                .build()

        assertThat(transitionHandler.startAnimation(Binder(), info, startT, finishT) {}).isTrue()
    }

    @Test
    fun startAnimation_nonLaunchingSystemModal_doesNotAnimate() {
        val info =
            TransitionInfoBuilder(TRANSIT_OPEN)
                .addChange(TRANSIT_CHANGE, createSystemModalTask())
                .build()

        assertThat(transitionHandler.startAnimation(Binder(), info, startT, finishT) {}).isFalse()
    }

    @Test
    fun startAnimation_launchingFullscreenTask_doesNotAnimate() {
        val info =
            TransitionInfoBuilder(TRANSIT_OPEN)
                .addChange(TRANSIT_OPEN, createFullscreenTask())
                .build()

        assertThat(transitionHandler.startAnimation(Binder(), info, startT, finishT) {}).isFalse()
    }

    @Test
    fun startAnimation_closingSystemModal_animates() {
        val info =
            TransitionInfoBuilder(TRANSIT_CLOSE)
                .addChange(TRANSIT_CLOSE, createSystemModalTask())
                .build()

        assertThat(transitionHandler.startAnimation(Binder(), info, startT, finishT) {}).isTrue()
    }

    @Test
    fun startAnimation_closingFullscreenTask_doesNotAnimate() {
        val info =
            TransitionInfoBuilder(TRANSIT_CLOSE)
                .addChange(TRANSIT_CLOSE, createFullscreenTask())
                .build()

        assertThat(transitionHandler.startAnimation(Binder(), info, startT, finishT) {}).isFalse()
    }

    @Test
    fun startAnimation_closingPreviouslyLaunchedSystemModal_animates() {
        val systemModalTask = createSystemModalTask()
        val nonModalSystemModalTask =
            createFullscreenTaskBuilder().setTaskId(systemModalTask.taskId).build()
        val launchInfo =
            TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_OPEN, systemModalTask).build()
        transitionHandler.startAnimation(Binder(), launchInfo, startT, finishT) {}
        val closeInfo =
            TransitionInfoBuilder(TRANSIT_CLOSE)
                .addChange(TRANSIT_CLOSE, nonModalSystemModalTask)
                .build()

        assertThat(transitionHandler.startAnimation(Binder(), closeInfo, startT, finishT) {})
            .isTrue()
    }
}