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

Commit c53d3124 authored by Saho Kobayashi's avatar Saho Kobayashi Committed by Android (Google) Code Review
Browse files

Merge "Update focus on task close on TransitionReady" into main

parents edcd7eaa f6b50d22
Loading
Loading
Loading
Loading
+31 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.wm.shell.dagger;

import static android.window.DesktopExperienceFlags.ENABLE_INORDER_TRANSITION_CALLBACKS_FOR_DESKTOP;
import static android.window.DesktopExperienceFlags.ENABLE_MULTI_DISPLAY_HOME_FOCUS_BUG_FIX;
import static android.window.DesktopExperienceFlags.ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS;
import static android.window.DesktopModeFlags.ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS;
import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX;
@@ -133,6 +134,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.desktopmode.DisplayDisconnectTransitionHandler;
import com.android.wm.shell.desktopmode.DisplayFocusResolver;
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
@@ -1102,7 +1104,8 @@ public abstract class WMShellModule {
            DesktopState desktopState,
            Optional<DesktopImeHandler> desktopImeHandler,
            Optional<DesktopBackNavTransitionObserver> desktopBackNavTransitionObserver,
            DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver) {
            DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
            Optional<DisplayFocusResolver> displayFocusResolver) {
        if (ENABLE_INORDER_TRANSITION_CALLBACKS_FOR_DESKTOP.isTrue()
                && ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS.isTrue()
                && desktopState.canEnterDesktopMode()) {
@@ -1112,7 +1115,8 @@ public abstract class WMShellModule {
                    desksTransitionObserver,
                    desktopImeHandler,
                    desktopBackNavTransitionObserver,
                    desktopModeLoggerTransitionObserver));
                    desktopModeLoggerTransitionObserver,
                    displayFocusResolver));
        }
        return Optional.empty();
    }
@@ -1583,6 +1587,31 @@ public abstract class WMShellModule {
                                        shellInit)));
    }

    @WMSingleton
    @Provides
    static Optional<DisplayFocusResolver> provideDisplayFocusResolver(
            Transitions transitions,
            ShellTaskOrganizer shellTaskOrganizer,
            FocusTransitionObserver focusTransitionObserver,
            DesktopState desktopState,
            Optional<DesktopUserRepositories> desktopUserRepositories,
            Optional<DesktopTasksController> desktopTasksController,
            ShellInit shellInit) {
        if (desktopUserRepositories.isPresent()
                && desktopTasksController.isPresent()
                && desktopState.canEnterDesktopMode()
                && ENABLE_MULTI_DISPLAY_HOME_FOCUS_BUG_FIX.isTrue()) {
            return Optional.of(
                    new DisplayFocusResolver(
                        transitions,
                        shellTaskOrganizer,
                        focusTransitionObserver,
                        desktopUserRepositories.get(),
                        desktopTasksController.get()));
        }
        return Optional.empty();
    }

    @WMSingleton
    @Provides
    static Optional<DesktopBackNavTransitionObserver> provideDesktopBackNavTransitionObserver(
+2 −0
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ class DesktopInOrderTransitionObserver(
    private val desktopImeHandler: Optional<DesktopImeHandler>,
    private val desktopBackNavTransitionObserver: Optional<DesktopBackNavTransitionObserver>,
    private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
    private val displayFocusResolver: Optional<DisplayFocusResolver>,
) : Transitions.TransitionObserver {

    override fun onTransitionReady(
@@ -64,6 +65,7 @@ class DesktopInOrderTransitionObserver(
        desktopImeHandler.ifPresent { it.onTransitionReady(transition, info) }
        desktopBackNavTransitionObserver.ifPresent { it.onTransitionReady(transition, info) }
        desktopModeLoggerTransitionObserver.onTransitionReady(transition, info, startT, finishT)
        displayFocusResolver.ifPresent { it.onTransitionReady(info) }
    }

    override fun onTransitionStarting(transition: IBinder) {
+177 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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

import android.app.ActivityManager.RunningTaskInfo
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.view.Display.INVALID_DISPLAY
import android.window.TransitionInfo
import android.window.TransitionInfo.FLAG_MOVED_TO_TOP
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.transition.FocusTransitionObserver
import com.android.wm.shell.transition.Transitions

/**
 * A [Transitions.TransitionObserver] that observes shell transitions to manage display focus in
 * desktop mode.
 *
 * When the last desktop task on a display is closed, this class prevents focus from returning to a
 * non-task surface (like the home screen) on that display. Instead, it attempts to move focus to a
 * desktop task on another display, if one exists.
 */
class DisplayFocusResolver(
    private val transitions: Transitions,
    private val shellTaskOrganizer: ShellTaskOrganizer,
    private val focusTransitionObserver: FocusTransitionObserver,
    private val desktopUserRepositories: DesktopUserRepositories,
    private val desktopTasksController: DesktopTasksController,
) {
    private var userId: Int = -1

    /**
     * Observes shell transitions to manage display focus.
     *
     * This method is called when a shell transition is ready. It inspects the transition to
     * determine if a desktop task is closing. If the closing task is the last one on its display,
     * this method may trigger a focus change to a task on another display to prevent the home
     * screen from taking focus.
     *
     * @param info The [TransitionInfo] for the transition, containing details about the windows
     *   involved.
     */
    fun onTransitionReady(info: TransitionInfo) {
        handleFocusOnTaskCloseIfNeeded(info)
    }

    private fun isDesktopTaskClosingTransitionOnDisplay(
        info: TransitionInfo,
        displayId: Int,
    ): Boolean {
        for (change in info.changes) {
            if (
                change.taskInfo != null &&
                    change.taskInfo?.taskId != INVALID_TASK_ID &&
                    change.taskInfo?.displayId == displayId &&
                    TransitionUtil.isClosingMode(change.mode)
            ) {
                userId = change.taskInfo!!.userId
                return true
            }
        }
        return false
    }

    /**
     * Finds the task that is being brought to the top on a given display during a transition. This
     * is identified by the [FLAG_MOVED_TO_TOP] flag.
     *
     * @return the [RunningTaskInfo] of the task being focused, or `null` if none is found.
     */
    private fun findNextFocusedTaskInDisplay(
        info: TransitionInfo,
        displayId: Int,
    ): RunningTaskInfo? {
        val change =
            info.changes.firstOrNull {
                it.endDisplayId == displayId && (it.flags and FLAG_MOVED_TO_TOP) != 0
            }
        return change?.taskInfo
    }

    /**
     * Finds the task ID of the top-most expanded desktop task on any display other than the one
     * that is currently focused.
     *
     * @param currentFocusedDisplayId The display that should be excluded from the search.
     * @return The task ID of the next task to focus, or [INVALID_TASK_ID] if none is found.
     */
    private fun findNextTopDesktopWindowGlobally(currentFocusedDisplayId: Int): Int {
        val desktopRepository = desktopUserRepositories.getProfile(userId)
        for (deskId in desktopRepository.getAllDeskIds()) {
            if (!desktopRepository.isDeskActive(deskId)) continue
            val expandedTasksInDesk = desktopRepository.getExpandedTasksIdsInDeskOrdered(deskId)
            if (!expandedTasksInDesk.isEmpty()) {
                if (desktopRepository.getDisplayForDesk(deskId) == currentFocusedDisplayId) {
                    // This should not happen. If the closing task's display is now empty of
                    // desktop tasks, the repository should reflect that. This is a safeguard.
                    ProtoLog.e(
                        WM_SHELL_DESKTOP_MODE,
                        "Unexpected task found in $currentFocusedDisplayId. This display is " +
                            "empty according to TransitionInfo but not empty in" +
                            "DesktopRepository",
                    )
                    continue
                }
                return expandedTasksInDesk.first()
            }
        }
        return INVALID_TASK_ID
    }

    /**
     * When a desktop task is closed, checks if focus should be moved to a task on another display.
     * This is done if the closing task was the last standard task on its display, preventing focus
     * from falling back to the home screen.
     */
    private fun handleFocusOnTaskCloseIfNeeded(info: TransitionInfo) {
        // TODO: b/436407117 - Re-evaluate with pressing home button scenario.
        val currentFocusDisplayId = focusTransitionObserver.globallyFocusedDisplayId
        if (
            currentFocusDisplayId == INVALID_DISPLAY ||
                !isDesktopTaskClosingTransitionOnDisplay(info, currentFocusDisplayId)
        ) {
            // This logic only applies when a desktop task is closing on the currently focused
            // display.
            return
        }

        // TODO: b/435099775 - Support DesktopWallpaperActivity
        val nextFocusedTaskInfo = findNextFocusedTaskInDisplay(info, currentFocusDisplayId)
        if (
            nextFocusedTaskInfo == null ||
                nextFocusedTaskInfo.topActivityType == ACTIVITY_TYPE_STANDARD
        ) {
            // If another standard task is becoming focused on the same display, let it proceed.
            // We only want to intervene if focus is falling back to a non-standard task,
            // like the home screen.
            return
        }

        // The focused display is now empty of standard tasks. Find a desktop task on another
        // display to focus instead.
        val newFocusedTaskId = findNextTopDesktopWindowGlobally(currentFocusDisplayId)
        if (newFocusedTaskId != INVALID_TASK_ID) {
            ProtoLog.e(
                WM_SHELL_DESKTOP_MODE,
                "Display $currentFocusDisplayId became empty. Moving focus to other display",
            )
            transitions.runOnIdle {
                desktopTasksController.moveTaskToFront(
                    taskId = newFocusedTaskId,
                    userId = userId,
                    remoteTransition = null,
                    unminimizeReason = UnminimizeReason.UNKNOWN,
                )
            }
        }
    }
}
+18 −0
Original line number Diff line number Diff line
@@ -52,6 +52,7 @@ class DesktopInOrderTransitionObserverTest : ShellTestCase() {
    private val desktopImeHandler = mock<DesktopImeHandler>()
    private val desktopBackNavTransitionObserver = mock<DesktopBackNavTransitionObserver>()
    private val desktopModeLoggerTransitionObserver = mock<DesktopModeLoggerTransitionObserver>()
    private val displayFocusResolver = mock<DisplayFocusResolver>()
    private lateinit var transitionObserver: DesktopInOrderTransitionObserver

    @Before
@@ -64,6 +65,7 @@ class DesktopInOrderTransitionObserverTest : ShellTestCase() {
                Optional.of(desktopImeHandler),
                Optional.of(desktopBackNavTransitionObserver),
                desktopModeLoggerTransitionObserver,
                Optional.of(displayFocusResolver),
            )
    }

@@ -188,4 +190,20 @@ class DesktopInOrderTransitionObserverTest : ShellTestCase() {
            .verify(desktopModeLoggerTransitionObserver)
            .onTransitionFinished(transition, aborted)
    }

    @Test
    @EnableFlags(
        Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP,
        Flags.FLAG_ENABLE_INORDER_TRANSITION_CALLBACKS_FOR_DESKTOP,
    )
    fun onTransitionReady_forwardsToDisplayFocusResolver() {
        val transition = Mockito.mock(IBinder::class.java)
        val info = TransitionInfoBuilder(TRANSIT_CHANGE, 0).build()
        val startT = mock<SurfaceControl.Transaction>()
        val finishT = mock<SurfaceControl.Transaction>()

        transitionObserver.onTransitionReady(transition, info, startT, finishT)

        verify(displayFocusResolver).onTransitionReady(info)
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ object DesktopTestHelpers {
            .setParentTaskId(displayId)
            .setToken(MockToken().token())
            .setActivityType(ACTIVITY_TYPE_STANDARD)
            .setTopActivityType(ACTIVITY_TYPE_STANDARD)
            .setWindowingMode(WINDOWING_MODE_FREEFORM)
            .setLastActiveTime(100)
            .setUserId(userId)
@@ -111,6 +112,7 @@ object DesktopTestHelpers {
            .setDisplayId(displayId)
            .setToken(MockToken().token())
            .setActivityType(ACTIVITY_TYPE_HOME)
            .setTopActivityType(ACTIVITY_TYPE_HOME)
            .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
            .setUserId(userId)
            .setLastActiveTime(100)
Loading