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

Commit f6b50d22 authored by Saho Kobayashi's avatar Saho Kobayashi
Browse files

Update focus on task close on TransitionReady

Expected user journey:
Task0(Focused) on Display0
Task1(Not Focused) on Display1
Close Task0

Current:
Display focus stays in Display0

With this change:
Display focus moves to Display1

Bug: 405297066
Test: DisplayFocusResolverTest
Flag: com.android.window.flags.enable_multi_display_home_focus_bug_fix
Change-Id: Iee1ef1688f260de942111b6ed40ca74ad27faac5
parent d5437349
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;
@@ -132,6 +133,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;
@@ -1101,7 +1103,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()) {
@@ -1111,7 +1114,8 @@ public abstract class WMShellModule {
                    desksTransitionObserver,
                    desktopImeHandler,
                    desktopBackNavTransitionObserver,
                    desktopModeLoggerTransitionObserver));
                    desktopModeLoggerTransitionObserver,
                    displayFocusResolver));
        }
        return Optional.empty();
    }
@@ -1566,6 +1570,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)
@@ -106,6 +107,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