Merge "Register all apps key gesture event in quickstep launcher" into main

This commit is contained in:
Treehugger Robot
2025-04-10 09:57:21 -07:00
committed by Android (Google) Code Review
8 changed files with 314 additions and 22 deletions
+3
View File
@@ -49,6 +49,9 @@
-->
<uses-permission android:name="android.permission.ACCESS_HIDDEN_PROFILES_FULL" />
<!-- Permission required to listen to recent, all app key gestures. -->
<uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" />
<!-- Permission required to start a WidgetPickerActivity. -->
<permission android:name="${applicationId}.permission.START_WIDGET_PICKER_ACTIVITY"
android:protectionLevel="signature|privileged" />
@@ -34,6 +34,7 @@ import static com.android.launcher3.util.DisplayController.CHANGE_DESKTOP_MODE;
import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
import static com.android.launcher3.util.DisplayController.CHANGE_SHOW_LOCKED_TASKBAR;
import static com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
import static com.android.quickstep.util.SystemActionConstants.ACTION_SHOW_TASKBAR;
@@ -44,12 +45,16 @@ import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.ComponentCallbacks;
import android.content.Context;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.hardware.display.DisplayManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Trace;
import android.provider.Settings;
import android.util.ArraySet;
@@ -116,6 +121,12 @@ public class TaskbarManager implements DisplayDecorationListener {
// TODO: b/397738606 - Remove all logs with this tag after the growth framework is integrated.
public static final String GROWTH_FRAMEWORK_TAG = "Growth Framework";
/**
* An integer extra specifying the ID of the display on which the All Apps UI should be shown
* or hidden.
*/
public static final String EXTRA_KEY_ALL_APPS_ACTION_DISPLAY_ID =
"com.android.quickstep.allapps.display_id";
/**
* All the configurations which do not initiate taskbar recreation.
@@ -575,10 +586,19 @@ public class TaskbarManager implements DisplayDecorationListener {
}
/**
* Toggles All Apps for Taskbar or Launcher depending on the current state.
* Shows or hides the All Apps view in the Taskbar or Launcher, based on its current
* visibility on the System UI tracked focused display.
*/
public void toggleAllAppsSearch() {
TaskbarActivityContext taskbar = getTaskbarForDisplay(getFocusedDisplayId());
toggleAllAppsSearchForDisplay(getFocusedDisplayId());
}
/**
* Shows or hides the All Apps view in the Taskbar or Launcher, based on its current
* visibility on the given display, with ID {@code displayId}.
*/
public void toggleAllAppsSearchForDisplay(int displayId) {
TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
if (taskbar == null) {
// Home All Apps should be toggled from this class, because the controllers are not
// initialized when Taskbar is disabled (i.e. TaskbarActivityContext is null).
@@ -1713,6 +1733,28 @@ public class TaskbarManager implements DisplayDecorationListener {
debugTaskbarManager(debugReason, mPrimaryDisplayId, verbose);
}
/** Creates a {@link PendingIntent} for showing / hiding the all apps UI. */
public PendingIntent createAllAppsPendingIntent() {
return new PendingIntent(new IIntentSender.Stub() {
@Override
public void send(int code, Intent intent, String resolvedType,
IBinder allowlistToken, IIntentReceiver finishedReceiver,
String requiredPermission, Bundle options) {
MAIN_EXECUTOR.execute(() -> {
int displayId = -1;
if (options != null) {
displayId = options.getInt(EXTRA_KEY_ALL_APPS_ACTION_DISPLAY_ID, -1);
}
if (displayId == -1) {
toggleAllAppsSearch();
} else {
toggleAllAppsSearchForDisplay(displayId);
}
});
}
});
}
/**
* Logs verbose debug information about the TaskbarManager for a specific display.
*/
@@ -27,6 +27,7 @@ import android.view.accessibility.AccessibilityManager
import com.android.launcher3.R
import com.android.launcher3.util.SettingsCache
import com.android.launcher3.util.SettingsCache.OnChangeListener
import com.android.quickstep.input.QuickstepKeyGestureEventsManager
import java.util.concurrent.Executor
private val USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor(USER_SETUP_COMPLETE)
@@ -41,6 +42,7 @@ private val USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor(USER_SETUP_COMPL
class AllAppsActionManager(
private val context: Context,
private val bgExecutor: Executor,
private val quickstepKeyGestureEventsManager: QuickstepKeyGestureEventsManager,
private val createAllAppsPendingIntent: () -> PendingIntent,
) {
@@ -92,17 +94,22 @@ class AllAppsActionManager(
val accessibilityManager =
context.getSystemService(AccessibilityManager::class.java) ?: return@execute
if (shouldRegisterAction) {
val allAppsPendingIntent = createAllAppsPendingIntent()
accessibilityManager.registerSystemAction(
RemoteAction(
Icon.createWithResource(context, R.drawable.ic_apps),
context.getString(R.string.all_apps_label),
context.getString(R.string.all_apps_label),
createAllAppsPendingIntent(),
allAppsPendingIntent,
),
GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS,
)
quickstepKeyGestureEventsManager.registerAllAppsKeyGestureEvent(
allAppsPendingIntent
)
} else {
accessibilityManager.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS)
quickstepKeyGestureEventsManager.unregisterAllAppsKeyGestureEvent()
}
}
}
@@ -42,11 +42,8 @@ import static com.android.quickstep.InputConsumerUtils.newConsumer;
import static com.android.quickstep.InputConsumerUtils.tryCreateAssistantInputConsumer;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Region;
@@ -101,6 +98,7 @@ import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
import com.android.quickstep.fallback.window.RecentsDisplayModel;
import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
import com.android.quickstep.input.QuickstepKeyGestureEventsManager;
import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
import com.android.quickstep.util.ActiveGestureLog;
@@ -639,6 +637,8 @@ public class TouchInteractionService extends Service {
private DisplayRepository mDisplayRepository;
private QuickstepKeyGestureEventsManager mQuickstepKeyGestureEventsHandler;
@Override
public void onCreate() {
super.onCreate();
@@ -653,8 +653,10 @@ public class TouchInteractionService extends Service {
mRotationTouchHelperRepository = RotationTouchHelper.REPOSITORY_INSTANCE.get(this);
mRecentsDisplayModel = RecentsDisplayModel.getINSTANCE().get(this);
mSystemDecorationChangeObserver = SystemDecorationChangeObserver.getINSTANCE().get(this);
mAllAppsActionManager = new AllAppsActionManager(
this, UI_HELPER_EXECUTOR, this::createAllAppsPendingIntent);
mQuickstepKeyGestureEventsHandler = new QuickstepKeyGestureEventsManager(this);
mAllAppsActionManager = new AllAppsActionManager(this, UI_HELPER_EXECUTOR,
mQuickstepKeyGestureEventsHandler,
() -> mTaskbarManager.createAllAppsPendingIntent());
mTrackpadsConnected = new ActiveTrackpadList(this, () -> {
if (mInputMonitorCompat != null && !mTrackpadsConnected.isEmpty()) {
// Don't destroy and reinitialize input monitor due to trackpad
@@ -812,17 +814,6 @@ public class TouchInteractionService extends Service {
}
}
private PendingIntent createAllAppsPendingIntent() {
return new PendingIntent(new IIntentSender.Stub() {
@Override
public void send(int code, Intent intent, String resolvedType,
IBinder allowlistToken, IIntentReceiver finishedReceiver,
String requiredPermission, Bundle options) {
MAIN_EXECUTOR.execute(() -> mTaskbarManager.toggleAllAppsSearch());
}
});
}
@UiThread
private void onSystemUiFlagsChanged(@SystemUiStateFlags long lastSysUIFlags, int displayId) {
if (LockedUserState.get(this).isUserUnlocked()) {
@@ -0,0 +1,79 @@
/*
* 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.quickstep.input
import android.app.PendingIntent
import android.content.Context
import android.hardware.input.InputManager
import android.hardware.input.InputManager.KeyGestureEventHandler
import android.hardware.input.KeyGestureEvent
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.launcher3.taskbar.TaskbarManager
import com.android.window.flags.Flags
/**
* Manages subscription and unsubscription to launcher's key gesture events, e.g. all apps and
* recents (incl. alt + tab).
*/
class QuickstepKeyGestureEventsManager(context: Context) {
private val inputManager = requireNotNull(context.getSystemService(InputManager::class.java))
private var allAppsPendingIntent: PendingIntent? = null
@VisibleForTesting
val allAppsKeyGestureEventHandler =
object : KeyGestureEventHandler {
override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?) {
if (!Flags.enableKeyGestureHandlerForRecents()) {
return
}
if (event.keyGestureType != KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) {
Log.e(TAG, "Ignore unsupported key gesture event type: ${event.keyGestureType}")
return
}
allAppsPendingIntent?.send(
Bundle().apply {
putInt(TaskbarManager.EXTRA_KEY_ALL_APPS_ACTION_DISPLAY_ID, event.displayId)
}
)
}
}
/** Registers the all apps key gesture events. */
fun registerAllAppsKeyGestureEvent(allAppsPendingIntent: PendingIntent) {
if (Flags.enableKeyGestureHandlerForRecents()) {
this.allAppsPendingIntent = allAppsPendingIntent
inputManager.registerKeyGestureEventHandler(
listOf(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS),
allAppsKeyGestureEventHandler,
)
}
}
/** Unregisters the all apps key gesture events. */
fun unregisterAllAppsKeyGestureEvent() {
if (Flags.enableKeyGestureHandlerForRecents()) {
inputManager.unregisterKeyGestureEventHandler(allAppsKeyGestureEventHandler)
}
}
private companion object {
const val TAG = "KeyGestureEventsHandler"
}
}
@@ -35,6 +35,7 @@ import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
import com.android.launcher3.util.TestUtil
import com.android.quickstep.AllAppsActionManager
import com.android.quickstep.fallback.window.RecentsDisplayModel
import com.android.quickstep.input.QuickstepKeyGestureEventsManager
import java.lang.reflect.Field
import java.lang.reflect.ParameterizedType
import java.util.Locale
@@ -108,7 +109,11 @@ class TaskbarUnitTestRule(
object :
TaskbarManager(
context,
AllAppsActionManager(context, UI_HELPER_EXECUTOR) {
AllAppsActionManager(
context,
UI_HELPER_EXECUTOR,
QuickstepKeyGestureEventsManager(context),
) {
PendingIntent(IIntentSender.Default())
},
object : TaskbarNavButtonCallbacks {},
@@ -18,6 +18,7 @@ package com.android.quickstep
import android.app.PendingIntent
import android.content.IIntentSender
import android.hardware.input.InputManager
import android.provider.Settings
import android.provider.Settings.Secure.USER_SETUP_COMPLETE
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -29,6 +30,7 @@ import com.android.launcher3.util.SandboxApplication
import com.android.launcher3.util.SettingsCache
import com.android.launcher3.util.SettingsCacheSandbox
import com.android.launcher3.util.TestUtil
import com.android.quickstep.input.QuickstepKeyGestureEventsManager
import com.google.common.truth.Truth.assertThat
import dagger.BindsInstance
import dagger.Component
@@ -39,6 +41,11 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.whenever
private const val TIMEOUT = 5L
private val USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor(USER_SETUP_COMPLETE)
@@ -49,24 +56,29 @@ class AllAppsActionManagerTest {
private val bgExecutor = UI_HELPER_EXECUTOR
@get:Rule val context = SandboxApplication()
private val inputManager = context.spyService(InputManager::class.java)
private val settingsCacheSandbox =
SettingsCacheSandbox().also { it[USER_SETUP_COMPLETE_URI] = 1 }
private val quickstepKeyGestureEventsManager = spy(QuickstepKeyGestureEventsManager(context))
private val allAppsActionManager by
lazy(LazyThreadSafetyMode.NONE) {
AllAppsActionManager(context, bgExecutor) {
AllAppsActionManager(context, bgExecutor, quickstepKeyGestureEventsManager) {
callbackSemaphore.release()
PendingIntent(IIntentSender.Default())
}
}
@Before
fun initDaggerComponent() {
fun setUp() {
context.initDaggerComponent(
DaggerAllAppsActionManagerTestComponent.builder()
.bindSettingsCache(settingsCacheSandbox.cache)
)
doNothing().whenever(inputManager).registerKeyGestureEventHandler(any(), any())
doNothing().whenever(inputManager).unregisterKeyGestureEventHandler(any())
}
@After fun destroyManager() = allAppsActionManager.onDestroy()
@@ -74,15 +86,19 @@ class AllAppsActionManagerTest {
@Test
fun taskbarPresent_actionRegistered() {
allAppsActionManager.isTaskbarPresent = true
TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to register.
assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
assertThat(allAppsActionManager.isActionRegistered).isTrue()
verify(quickstepKeyGestureEventsManager).registerAllAppsKeyGestureEvent(any())
}
@Test
fun homeAndOverviewSame_actionRegistered() {
allAppsActionManager.isHomeAndOverviewSame = true
TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to register.
assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
assertThat(allAppsActionManager.isActionRegistered).isTrue()
verify(quickstepKeyGestureEventsManager).registerAllAppsKeyGestureEvent(any())
}
@Test
@@ -93,6 +109,7 @@ class AllAppsActionManagerTest {
allAppsActionManager.isTaskbarPresent = false
TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to unregister.
assertThat(allAppsActionManager.isActionRegistered).isFalse()
verify(quickstepKeyGestureEventsManager).unregisterAllAppsKeyGestureEvent()
}
@Test
@@ -103,6 +120,7 @@ class AllAppsActionManagerTest {
TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to unregister.
assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
assertThat(allAppsActionManager.isActionRegistered).isFalse()
verify(quickstepKeyGestureEventsManager).unregisterAllAppsKeyGestureEvent()
}
@Test
@@ -136,8 +154,10 @@ class AllAppsActionManagerTest {
allAppsActionManager.isTaskbarPresent = true
settingsCacheSandbox[USER_SETUP_COMPLETE_URI] = 1
TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to register.
assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
assertThat(allAppsActionManager.isActionRegistered).isTrue()
verify(quickstepKeyGestureEventsManager).registerAllAppsKeyGestureEvent(any())
}
@Test
@@ -146,8 +166,10 @@ class AllAppsActionManagerTest {
allAppsActionManager.isTaskbarPresent = true
allAppsActionManager.isSetupUiVisible = false
TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to register.
assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
assertThat(allAppsActionManager.isActionRegistered).isTrue()
verify(quickstepKeyGestureEventsManager).registerAllAppsKeyGestureEvent(any())
}
}
@@ -0,0 +1,143 @@
/*
* 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.quickstep.input
import android.app.PendingIntent
import android.hardware.input.InputManager
import android.hardware.input.KeyGestureEvent
import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS
import android.os.Bundle
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.taskbar.TaskbarManager.EXTRA_KEY_ALL_APPS_ACTION_DISPLAY_ID
import com.android.launcher3.util.SandboxApplication
import com.android.window.flags.Flags
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.KArgumentCaptor
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doNothing
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
@SmallTest
@RunWith(AndroidJUnit4::class)
class QuickstepKeyGestureEventsHandlerTest {
@get:Rule val context = SandboxApplication()
@get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT)
private val inputManager = context.spyService(InputManager::class.java)
private val keyGestureEventsManager = QuickstepKeyGestureEventsManager(context)
private val allAppsPendingIntent: PendingIntent = mock()
private val keyGestureEventsCaptor: KArgumentCaptor<List<Int>> = argumentCaptor()
private val bundleCaptor: KArgumentCaptor<Bundle> = argumentCaptor()
@Before
fun setup() {
doNothing().whenever(inputManager).registerKeyGestureEventHandler(any(), any())
doNothing().whenever(inputManager).unregisterKeyGestureEventHandler(any())
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_KEY_GESTURE_HANDLER_FOR_RECENTS)
fun registerKeyGestureEventsHandler_flagEnabled_registerWithExpectedKeyGestureEvents() {
keyGestureEventsManager.registerAllAppsKeyGestureEvent(allAppsPendingIntent)
verify(inputManager)
.registerKeyGestureEventHandler(
keyGestureEventsCaptor.capture(),
eq(keyGestureEventsManager.allAppsKeyGestureEventHandler),
)
assertThat(keyGestureEventsCaptor.firstValue).containsExactly(KEY_GESTURE_TYPE_ALL_APPS)
}
@Test
@DisableFlags(Flags.FLAG_ENABLE_KEY_GESTURE_HANDLER_FOR_RECENTS)
fun registerKeyGestureEventsHandler_flagDisabled_noRegister() {
keyGestureEventsManager.registerAllAppsKeyGestureEvent(allAppsPendingIntent)
verifyNoInteractions(inputManager)
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_KEY_GESTURE_HANDLER_FOR_RECENTS)
fun unregisterKeyGestureEventsHandler_flagEnabled_unregisterHandler() {
keyGestureEventsManager.unregisterAllAppsKeyGestureEvent()
verify(inputManager)
.unregisterKeyGestureEventHandler(
eq(keyGestureEventsManager.allAppsKeyGestureEventHandler)
)
}
@Test
@DisableFlags(Flags.FLAG_ENABLE_KEY_GESTURE_HANDLER_FOR_RECENTS)
fun unregisterKeyGestureEventsHandler_flagDisabled_noUnregister() {
keyGestureEventsManager.unregisterAllAppsKeyGestureEvent()
verifyNoInteractions(inputManager)
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_KEY_GESTURE_HANDLER_FOR_RECENTS)
fun handleEvent_flagEnabled_allApps_toggleAllAppsSearchWithDisplayId() {
keyGestureEventsManager.registerAllAppsKeyGestureEvent(allAppsPendingIntent)
keyGestureEventsManager.allAppsKeyGestureEventHandler.handleKeyGestureEvent(
KeyGestureEvent.Builder()
.setDisplayId(TEST_DISPLAY_ID)
.setKeyGestureType(KEY_GESTURE_TYPE_ALL_APPS)
.build(),
/* focusedToken= */ null,
)
verify(allAppsPendingIntent).send(bundleCaptor.capture())
assertThat(bundleCaptor.firstValue.getInt(EXTRA_KEY_ALL_APPS_ACTION_DISPLAY_ID))
.isEqualTo(TEST_DISPLAY_ID)
}
@Test
@DisableFlags(Flags.FLAG_ENABLE_KEY_GESTURE_HANDLER_FOR_RECENTS)
fun handleEvent_flagDisabled_allApps_noInteractionWithTaskbar() {
keyGestureEventsManager.registerAllAppsKeyGestureEvent(allAppsPendingIntent)
keyGestureEventsManager.allAppsKeyGestureEventHandler.handleKeyGestureEvent(
KeyGestureEvent.Builder()
.setDisplayId(TEST_DISPLAY_ID)
.setKeyGestureType(KEY_GESTURE_TYPE_ALL_APPS)
.build(),
/* focusedToken= */ null,
)
verifyNoInteractions(allAppsPendingIntent)
}
private companion object {
const val TEST_DISPLAY_ID = 6789
}
}