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

Commit d678aab7 authored by Vaibhav Devmurari's avatar Vaibhav Devmurari Committed by Android (Google) Code Review
Browse files

Merge "Shift quit key gesture handling to Shell" into main

parents 2c4d18c0 bd101d5b
Loading
Loading
Loading
Loading
+101 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.common

import android.annotation.SuppressLint
import android.app.ActivityTaskManager
import android.app.IActivityTaskManager
import android.content.Context
import android.hardware.input.InputManager
import android.hardware.input.KeyGestureEvent
import android.os.IBinder
import android.os.RemoteException
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.desktopmode.DesktopModeKeyGestureHandler
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.transition.FocusTransitionObserver
import java.util.Optional

/** Handles key gesture events to quit currently focused app. */
@SuppressLint("MissingPermission")
class QuitFocusedAppKeyGestureHandler(
    val context: Context,
    inputManager: InputManager,
    val displayController: DisplayController,
    val lockTaskChangeListener: LockTaskChangeListener,
    val desktopModeKeyGestureHandler: Optional<DesktopModeKeyGestureHandler>,
    val activityTaskManagerService: IActivityTaskManager,
    val focusTransitionObserver: FocusTransitionObserver,
    @ShellMainThread val executor: ShellExecutor
) : InputManager.KeyGestureEventHandler {
    init {
        inputManager.registerKeyGestureEventHandler(
            listOf(KeyGestureEvent.KEY_GESTURE_TYPE_QUIT_FOCUSED_TASK),
            this
        )
    }

    override fun handleKeyGestureEvent(
        event: KeyGestureEvent,
        focusedToken: IBinder?
    ) {
        if (event.keyGestureType != KeyGestureEvent.KEY_GESTURE_TYPE_QUIT_FOCUSED_TASK) {
            logW("Unsupported key gesture received $event")
            return
        }
        if (event.action == KeyGestureEvent.ACTION_GESTURE_START || event.isCancelled) {
            logV("Quit focused app gesture not complete or cancelled")
            return
        }
        val handler = desktopModeKeyGestureHandler.orElse(null)
        if (handler != null && handler.quitFocusedDesktopTask()) {
            logV("Closed focused desktop task")
            return
        }
        if (lockTaskChangeListener.isTaskLocked) {
            logW("Device in lock task mode: Unable to quit")
            return
        }
        try {
            val focusedTaskId = focusTransitionObserver.globallyFocusedTaskId
            if (focusedTaskId == ActivityTaskManager.INVALID_TASK_ID) {
                logW("The global focused task not found: Unable to quit")
                return
            }
            activityTaskManagerService.removeTask(focusedTaskId)
        } catch (e: RemoteException) {
            logE("Unable to quit focused task $e")
        }
    }

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

    private fun logW(msg: String, vararg arguments: Any?) {
        ProtoLog.w(WM_SHELL, "%s: $msg", TAG, *arguments)
    }

    private fun logE(msg: String, vararg arguments: Any?) {
        ProtoLog.e(WM_SHELL, "%s: $msg", TAG, *arguments)
    }

    companion object {
        private const val TAG = "QuitFocusedAppKeyGestureHandler"
    }
}
 No newline at end of file
+19 −1
Original line number Diff line number Diff line
@@ -92,6 +92,7 @@ import com.android.wm.shell.common.LockTaskChangeListener;
import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController;
import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorSurface;
import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.QuitFocusedAppKeyGestureHandler;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TaskStackListenerImpl;
@@ -1271,6 +1272,22 @@ public abstract class WMShellModule {
        return Optional.empty();
    }

    @WMSingleton
    @Provides
    static QuitFocusedAppKeyGestureHandler provideQuitFocusedAppKeyGestureHandler(
            Context context,
            InputManager inputManager,
            DisplayController displayController,
            LockTaskChangeListener lockTaskChangeListener,
            Optional<DesktopModeKeyGestureHandler> desktopModeKeyGestureHandler,
            IActivityTaskManager activityTaskManagerService,
            FocusTransitionObserver focusTransitionObserver,
            @ShellMainThread ShellExecutor mainExecutor) {
        return new QuitFocusedAppKeyGestureHandler(context, inputManager, displayController,
                lockTaskChangeListener, desktopModeKeyGestureHandler, activityTaskManagerService,
                focusTransitionObserver, mainExecutor);
    }

    @WMSingleton
    @Provides
    static Optional<DesktopModeWindowDecorViewModel> provideDesktopModeWindowDecorViewModel(
@@ -2032,7 +2049,8 @@ public abstract class WMShellModule {
            Optional<DisplayDisconnectTransitionHandler> displayDisconnectTransitionHandler,
            Optional<DesktopImeHandler> desktopImeHandler,
            ShellCrashHandler shellCrashHandler,
            AppToWebEducationController appToWebEducationController) {
            AppToWebEducationController appToWebEducationController,
            QuitFocusedAppKeyGestureHandler quitFocusedAppKeyGestureHandler) {
        return new Object();
    }

+17 −15
Original line number Diff line number Diff line
@@ -149,9 +149,7 @@ class DesktopModeKeyGestureHandler(
                    val displayId = taskInfo.displayId
                    val displayLayout = displayController.getDisplayLayout(displayId)
                    if (displayLayout == null) {
                        logW(
                            "Display %d is not found, task displayId might be stale", displayId
                        )
                        logW("Display %d is not found, task displayId might be stale", displayId)
                        return
                    }
                    mainExecutor.execute {
@@ -179,18 +177,7 @@ class DesktopModeKeyGestureHandler(
            }
            KeyGestureEvent.KEY_GESTURE_TYPE_QUIT_FOCUSED_DESKTOP_TASK -> {
                logV("Key gesture KEY_GESTURE_TYPE_QUIT_FOCUSED_DESKTOP_TASK is handled")
                val focusedTask = getGloballyFocusedDesktopTask()
                if (focusedTask == null) {
                    logV(
                        "Globally focused desktop task is not found to close. focusedDisplay=%d",
                        focusTransitionObserver.globallyFocusedDisplayId,
                    )
                    return
                }
                logV("Found focused desktop task %d to close", focusedTask.taskId)
                mainExecutor.execute {
                    desktopModeWindowDecorViewModel.get().closeTask(focusedTask)
                }
                quitFocusedDesktopTask()
            }
            KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_FULLSCREEN -> {
                logV("Key gesture TOGGLE_FULLSCREEN is handled")
@@ -207,6 +194,21 @@ class DesktopModeKeyGestureHandler(
        }
    }

    /** Quits the focussed task in desktop mode */
    public fun quitFocusedDesktopTask(): Boolean {
        val focusedTask = getGloballyFocusedDesktopTask()
        if (focusedTask == null) {
            logV(
                "Globally focused desktop task is not found to close. focusedDisplayId=%d",
                focusTransitionObserver.globallyFocusedDisplayId,
            )
            return false
        }
        logV("Found focused desktop task %d to close", focusedTask.taskId)
        mainExecutor.execute { desktopModeWindowDecorViewModel.get().closeTask(focusedTask) }
        return true
    }

    //  TODO: b/364154795 - wait for the completion of moveToNextDisplay transition, otherwise it
    //  will pick a wrong task when a user quickly perform other actions with keyboard shortcuts
    //  after moveToNextDisplay, and move this to FocusTransitionObserver class.
+133 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.common

import android.app.IActivityTaskManager
import android.hardware.input.InputManager
import android.hardware.input.InputManager.KeyGestureEventHandler
import android.hardware.input.KeyGestureEvent
import android.testing.AndroidTestingRunner
import android.view.KeyEvent
import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.desktopmode.DesktopModeKeyGestureHandler
import com.android.wm.shell.transition.FocusTransitionObserver
import java.util.Optional
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever

/**
 * Test class for [QuitFocusedAppKeyGestureHandler]
 *
 * Usage: atest WMShellUnitTests:QuitFocusedAppKeyGestureHandler
 */
@SmallTest
@RunWith(AndroidTestingRunner::class)
class QuitFocusedAppKeyGestureHandlerTest : ShellTestCase() {

    private val inputManager = mock<InputManager>()
    private val displayController = mock<DisplayController>()
    private val lockTaskChangeListener = mock<LockTaskChangeListener>()
    private val desktopModeKeyHandler = mock<DesktopModeKeyGestureHandler>()
    private val activityTaskService = mock<IActivityTaskManager>()
    private val focusTransitionObserver = mock<FocusTransitionObserver>()
    private val mainExecutor = mock<ShellExecutor>()

    private lateinit var quitFocusedAppKeyGestureHandler: QuitFocusedAppKeyGestureHandler
    private lateinit var keyGestureEventHandler: KeyGestureEventHandler

    @Before
    fun setUp() {
        doAnswer {
            keyGestureEventHandler = (it.arguments[1] as KeyGestureEventHandler)
            null
        }.whenever(inputManager).registerKeyGestureEventHandler(any(), any())
        whenever(focusTransitionObserver.globallyFocusedTaskId).thenReturn(TASK_ID)
        quitFocusedAppKeyGestureHandler =
            QuitFocusedAppKeyGestureHandler(
                context,
                inputManager,
                displayController,
                lockTaskChangeListener,
                Optional.of(desktopModeKeyHandler),
                activityTaskService,
                focusTransitionObserver,
                mainExecutor
            )
    }

    @Test
    fun quitAppGesture_whenInDesktopMode_closesDesktopTask() {
        whenever(desktopModeKeyHandler.quitFocusedDesktopTask()).thenReturn(true)

        sendQuitAppGesture()

        verifyNoInteractions(lockTaskChangeListener)
        verifyNoInteractions(focusTransitionObserver)
        verifyNoInteractions(activityTaskService)
    }

    @Test
    fun quitAppGesture_whenInLockTaskMode_doesNothing() {
        whenever(desktopModeKeyHandler.quitFocusedDesktopTask()).thenReturn(false)
        whenever(lockTaskChangeListener.isTaskLocked).thenReturn(true)

        sendQuitAppGesture()

        verifyNoInteractions(focusTransitionObserver)
        verifyNoInteractions(activityTaskService)
    }

    @Test
    fun quitAppGesture_whenNotInDesktopMode_fallsBackToActivityManagerToCloseFocusedTask() {
        whenever(desktopModeKeyHandler.quitFocusedDesktopTask()).thenReturn(false)
        whenever(lockTaskChangeListener.isTaskLocked).thenReturn(false)

        sendQuitAppGesture()

        verify(activityTaskService).removeTask(eq(TASK_ID))
    }

    private fun sendQuitAppGesture() {
        keyGestureEventHandler.handleKeyGestureEvent(
            KeyGestureEvent.Builder()
                .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_QUIT_FOCUSED_TASK)
                .setKeycodes(intArrayOf(KeyEvent.KEYCODE_ESCAPE))
                .setAction(KeyGestureEvent.ACTION_GESTURE_START)
                .build(), /* focusedToken =*/null
        )
        keyGestureEventHandler.handleKeyGestureEvent(
            KeyGestureEvent.Builder()
                .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_QUIT_FOCUSED_TASK)
                .setKeycodes(intArrayOf(KeyEvent.KEYCODE_ESCAPE))
                .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
                .build(), /* focusedToken =*/null
        )
    }

    companion object {
        const val TASK_ID = 123
    }
}
 No newline at end of file
+11 −1
Original line number Diff line number Diff line
@@ -838,6 +838,10 @@ final class KeyGestureController {
                    Toast.makeText(mContext, UiThread.get().getLooper(),
                            mContext.getString(R.string.exit_toast_on_long_press_escape),
                            Toast.LENGTH_SHORT).show();
                    handleKeyGesture(event.getDeviceId(), new int[]{KeyEvent.KEYCODE_ESCAPE},
                            metaState, KeyGestureEvent.KEY_GESTURE_TYPE_QUIT_FOCUSED_TASK,
                            KeyGestureEvent.ACTION_GESTURE_START,
                            displayId, focusedToken, /* flags= */0, /* appLaunchData= */ null);
                    AidlKeyGestureEvent eventToSend = createKeyGestureEvent(event.getDeviceId(),
                            new int[]{KeyEvent.KEYCODE_ESCAPE},
                            metaState, KeyGestureEvent.KEY_GESTURE_TYPE_QUIT_FOCUSED_TASK,
@@ -846,7 +850,13 @@ final class KeyGestureController {
                    Message msg = Message.obtain(mHandler, MSG_EXIT_FOCUSED_APP, eventToSend);
                    mHandler.sendMessageDelayed(msg, LONG_PRESS_DURATION_FOR_EXIT_APP_MS);
                } else if (!down) {
                    if (mHandler.hasMessages(MSG_EXIT_FOCUSED_APP)) {
                        mHandler.removeMessages(MSG_EXIT_FOCUSED_APP);
                        handleKeyGesture(event.getDeviceId(), new int[]{KeyEvent.KEYCODE_ESCAPE},
                                metaState, KeyGestureEvent.KEY_GESTURE_TYPE_QUIT_FOCUSED_TASK,
                                KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken,
                                KeyGestureEvent.FLAG_CANCELLED, /* appLaunchData= */ null);
                    }
                }
                break;
            case KeyEvent.KEYCODE_ASSIST:
Loading