diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index ab6eafb0bf..a59aeada5f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -413,6 +413,13 @@ public class LauncherTaskbarUIController extends TaskbarUIController { return mTaskbarLauncherStateController.isInOverview(); } + @Override + protected boolean canToggleHomeAllApps() { + return mLauncher.isResumed() + && !mTaskbarLauncherStateController.isInOverview() + && !mLauncher.areFreeformTasksVisible(); + } + @Override public RecentsView getRecentsView() { return mLauncher.getOverviewPanel(); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index 557c009b4e..26212c1934 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -1560,4 +1560,13 @@ public class TaskbarActivityContext extends BaseTaskbarContext { public float getStashedTaskbarScale() { return mControllers.stashedHandleViewController.getStashedHandleHintScale().value; } + + /** Closes the KeyboardQuickSwitchView without an animation if open. */ + public void closeKeyboardQuickSwitchView() { + mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false); + } + + boolean canToggleHomeAllApps() { + return mControllers.uiController.canToggleHomeAllApps(); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java index 1b03e7a3ea..e4f9ba5260 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java @@ -23,7 +23,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static com.android.launcher3.BaseActivity.EVENT_DESTROYED; import static com.android.launcher3.Flags.enableUnfoldStateAnimation; -import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate; import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY; @@ -67,6 +66,7 @@ import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.SettingsCache; import com.android.launcher3.util.SimpleBroadcastReceiver; +import com.android.quickstep.AllAppsActionManager; import com.android.quickstep.RecentsActivity; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TouchInteractionService; @@ -155,6 +155,8 @@ public class TaskbarManager { private final SimpleBroadcastReceiver mTaskbarBroadcastReceiver = new SimpleBroadcastReceiver(this::showTaskbarFromBroadcast); + private final AllAppsActionManager mAllAppsActionManager; + private final Runnable mActivityOnDestroyCallback = new Runnable() { @Override public void run() { @@ -205,12 +207,14 @@ public class TaskbarManager { }; @SuppressLint("WrongConstant") - public TaskbarManager(TouchInteractionService service) { + public TaskbarManager( + TouchInteractionService service, AllAppsActionManager allAppsActionManager) { Display display = service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY); mContext = service.createWindowContext(display, ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL, null); + mAllAppsActionManager = allAppsActionManager; mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION ? service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null) : null; @@ -261,10 +265,10 @@ public class TaskbarManager { recreateTaskbar(); } else { // Config change might be handled without re-creating the taskbar - if (dp != null && !isTaskbarPresent(dp)) { + if (dp != null && !isTaskbarEnabled(dp)) { destroyExistingTaskbar(); } else { - if (dp != null && isTaskbarPresent(dp)) { + if (dp != null && isTaskbarEnabled(dp)) { if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) { // Re-initialize for screen size change? Should this be done // by looking at screen-size change flag in configDiff in the @@ -319,7 +323,7 @@ public class TaskbarManager { } DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null; - if (dp == null || !isTaskbarPresent(dp)) { + if (dp == null || !isTaskbarEnabled(dp)) { removeTaskbarRootViewFromWindow(); } } @@ -339,20 +343,11 @@ public class TaskbarManager { * @param homeAllAppsIntent Intent used if Taskbar is not enabled or Launcher is resumed. */ public void toggleAllApps(Intent homeAllAppsIntent) { - if (mTaskbarActivityContext == null) { + if (mTaskbarActivityContext == null || mTaskbarActivityContext.canToggleHomeAllApps()) { mContext.startActivity(homeAllAppsIntent); - return; + } else { + mTaskbarActivityContext.toggleAllAppsSearch(); } - - if (mActivity != null - && mActivity.isResumed() - && !mActivity.isInState(OVERVIEW) - && !(mActivity instanceof QuickstepLauncher l && l.areFreeformTasksVisible())) { - mContext.startActivity(homeAllAppsIntent); - return; - } - - mTaskbarActivityContext.toggleAllAppsSearch(); } /** @@ -447,9 +442,12 @@ public class TaskbarManager { DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null; + // All Apps action is unrelated to navbar unification, so we only need to check DP. + mAllAppsActionManager.setTaskbarPresent(dp != null && dp.isTaskbarPresent); + destroyExistingTaskbar(); - boolean isTaskbarEnabled = dp != null && isTaskbarPresent(dp); + boolean isTaskbarEnabled = dp != null && isTaskbarEnabled(dp); debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled + " [dp != null (i.e. mUserUnlocked)]=" + (dp != null) + " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION @@ -514,7 +512,7 @@ public class TaskbarManager { } } - private static boolean isTaskbarPresent(DeviceProfile deviceProfile) { + private static boolean isTaskbarEnabled(DeviceProfile deviceProfile) { return ENABLE_TASKBAR_NAVBAR_UNIFICATION || deviceProfile.isTaskbarPresent; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java index efe1e39f97..109400ee6b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -197,6 +197,11 @@ public class TaskbarUIController { return false; } + /** Returns {@code true} if Home All Apps available instead of Taskbar All Apps. */ + protected boolean canToggleHomeAllApps() { + return false; + } + @CallSuper protected void dumpLogs(String prefix, PrintWriter pw) { pw.println(String.format( diff --git a/quickstep/src/com/android/quickstep/AllAppsActionManager.kt b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt new file mode 100644 index 0000000000..fd2ed3ac18 --- /dev/null +++ b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt @@ -0,0 +1,90 @@ +/* + * 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.quickstep + +import android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS +import android.app.PendingIntent +import android.app.RemoteAction +import android.content.Context +import android.graphics.drawable.Icon +import android.view.accessibility.AccessibilityManager +import com.android.launcher3.R +import java.util.concurrent.Executor + +/** + * Registers a [RemoteAction] for toggling All Apps if needed. + * + * We need this action when either [isHomeAndOverviewSame] or [isTaskbarPresent] is `true`. When + * home and overview are the same, we can control Launcher's or Taskbar's All Apps tray. If they are + * not the same, but Taskbar is present, we can only control Taskbar's tray. + */ +class AllAppsActionManager( + private val context: Context, + private val bgExecutor: Executor, + private val createAllAppsPendingIntent: () -> PendingIntent, +) { + + /** `true` if home and overview are the same Activity. */ + var isHomeAndOverviewSame = false + set(value) { + field = value + updateSystemAction() + } + + /** `true` if Taskbar is enabled. */ + var isTaskbarPresent = false + set(value) { + field = value + updateSystemAction() + } + + /** `true` if the action should be registered. */ + var isActionRegistered = false + private set + + private fun updateSystemAction() { + val shouldRegisterAction = isHomeAndOverviewSame || isTaskbarPresent + if (isActionRegistered == shouldRegisterAction) return + isActionRegistered = shouldRegisterAction + + bgExecutor.execute { + val accessibilityManager = + context.getSystemService(AccessibilityManager::class.java) ?: return@execute + if (shouldRegisterAction) { + 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(), + ), + GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, + ) + } else { + accessibilityManager.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS) + } + } + } + + fun onDestroy() { + context + .getSystemService(AccessibilityManager::class.java) + ?.unregisterSystemAction( + GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, + ) + } +} diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index 719c4f7f70..b43c5201ce 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -31,6 +31,7 @@ import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent; import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe; import static com.android.launcher3.config.FeatureFlags.ENABLE_TRACKPAD_GESTURE; 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.OnboardingPrefs.HOME_BOUNCE_SEEN; import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH; import static com.android.quickstep.GestureState.DEFAULT_STATE; @@ -59,14 +60,12 @@ import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SP import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW; import android.app.PendingIntent; -import android.app.RemoteAction; import android.app.Service; import android.content.IIntentReceiver; import android.content.IIntentSender; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Region; -import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.IBinder; import android.os.Looper; @@ -77,7 +76,6 @@ import android.view.Choreographer; import android.view.InputDevice; import android.view.InputEvent; import android.view.MotionEvent; -import android.view.accessibility.AccessibilityManager; import androidx.annotation.BinderThread; import androidx.annotation.NonNull; @@ -88,7 +86,6 @@ import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.ConstantItem; import com.android.launcher3.EncryptionType; import com.android.launcher3.LauncherPrefs; -import com.android.launcher3.R; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.provider.RestoreDbTask; @@ -101,7 +98,6 @@ import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.uioverrides.flags.FlagsFactory; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.DisplayController; -import com.android.launcher3.util.Executors; import com.android.launcher3.util.LockedUserState; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.ScreenOnTracker; @@ -488,6 +484,7 @@ public class TouchInteractionService extends Service { private TaskbarManager mTaskbarManager; private Function mSwipeUpProxyProvider = i -> null; + private AllAppsActionManager mAllAppsActionManager; @Override public void onCreate() { @@ -497,7 +494,9 @@ public class TouchInteractionService extends Service { mMainChoreographer = Choreographer.getInstance(); mAM = ActivityManagerWrapper.getInstance(); mDeviceState = new RecentsAnimationDeviceState(this, true); - mTaskbarManager = new TaskbarManager(this); + mAllAppsActionManager = new AllAppsActionManager( + this, UI_HELPER_EXECUTOR, this::createAllAppsPendingIntent); + mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager); mRotationTouchHelper = mDeviceState.getRotationTouchHelper(); mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer(); BootAwarePreloader.start(this); @@ -590,16 +589,7 @@ public class TouchInteractionService extends Service { } private void onOverviewTargetChange(boolean isHomeAndOverviewSame) { - Executors.UI_HELPER_EXECUTOR.execute(() -> { - AccessibilityManager am = getSystemService(AccessibilityManager.class); - - if (isHomeAndOverviewSame) { - am.registerSystemAction( - createAllAppsAction(), GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS); - } else { - am.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS); - } - }); + mAllAppsActionManager.setHomeAndOverviewSame(isHomeAndOverviewSame); StatefulActivity newOverviewActivity = mOverviewComponentObserver.getActivityInterface() .getCreatedActivity(); @@ -609,13 +599,12 @@ public class TouchInteractionService extends Service { mTISBinder.onOverviewTargetChange(); } - private RemoteAction createAllAppsAction() { + private PendingIntent createAllAppsPendingIntent() { final Intent homeIntent = new Intent(mOverviewComponentObserver.getHomeIntent()) .setAction(INTENT_ACTION_ALL_APPS_TOGGLE); - final PendingIntent actionPendingIntent; if (FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) { - actionPendingIntent = new PendingIntent(new IIntentSender.Stub() { + return new PendingIntent(new IIntentSender.Stub() { @Override public void send(int code, Intent intent, String resolvedType, IBinder allowlistToken, IIntentReceiver finishedReceiver, @@ -624,18 +613,12 @@ public class TouchInteractionService extends Service { } }); } else { - actionPendingIntent = PendingIntent.getActivity( + return PendingIntent.getActivity( this, GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, homeIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } - - return new RemoteAction( - Icon.createWithResource(this, R.drawable.ic_apps), - getString(R.string.all_apps_label), - getString(R.string.all_apps_label), - actionPendingIntent); } @UiThread @@ -678,8 +661,7 @@ public class TouchInteractionService extends Service { mDeviceState.destroy(); SystemUiProxy.INSTANCE.get(this).clearProxy(); - getSystemService(AccessibilityManager.class) - .unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS); + mAllAppsActionManager.onDestroy(); mTaskbarManager.destroy(); sConnected = false; diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt new file mode 100644 index 0000000000..73b35e8a5c --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt @@ -0,0 +1,91 @@ +/* + * 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.quickstep + +import android.app.PendingIntent +import android.content.IIntentSender +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR +import com.android.launcher3.util.TestUtil +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit.SECONDS +import org.junit.Test +import org.junit.runner.RunWith + +private const val TIMEOUT = 5L + +@RunWith(AndroidJUnit4::class) +class AllAppsActionManagerTest { + private val callbackSemaphore = Semaphore(0) + private val bgExecutor = UI_HELPER_EXECUTOR + + private val allAppsActionManager = + AllAppsActionManager( + InstrumentationRegistry.getInstrumentation().targetContext, + bgExecutor, + ) { + callbackSemaphore.release() + PendingIntent(IIntentSender.Default()) + } + + @Test + fun taskbarPresent_actionRegistered() { + allAppsActionManager.isTaskbarPresent = true + assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() + assertThat(allAppsActionManager.isActionRegistered).isTrue() + } + + @Test + fun homeAndOverviewSame_actionRegistered() { + allAppsActionManager.isHomeAndOverviewSame = true + assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() + assertThat(allAppsActionManager.isActionRegistered).isTrue() + } + + @Test + fun toggleTaskbar_destroyedAfterActionRegistered_actionUnregistered() { + allAppsActionManager.isTaskbarPresent = true + assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() + + allAppsActionManager.isTaskbarPresent = false + TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to unregister. + assertThat(allAppsActionManager.isActionRegistered).isFalse() + } + + @Test + fun toggleTaskbar_destroyedBeforeActionRegistered_pendingActionUnregistered() { + allAppsActionManager.isTaskbarPresent = true + allAppsActionManager.isTaskbarPresent = false + + TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to unregister. + assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() + assertThat(allAppsActionManager.isActionRegistered).isFalse() + } + + @Test + fun changeHome_sameAsOverviewBeforeActionUnregistered_actionRegisteredAgain() { + allAppsActionManager.isHomeAndOverviewSame = true // Initialize to same. + assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() + + allAppsActionManager.isHomeAndOverviewSame = false + allAppsActionManager.isHomeAndOverviewSame = true + assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() + assertThat(allAppsActionManager.isActionRegistered).isTrue() + } +}