From 0b936727d68183e1c25b1094d685870cad71df7c Mon Sep 17 00:00:00 2001 From: Andy Wickham Date: Tue, 30 Jul 2024 19:43:24 -0700 Subject: [PATCH 01/11] Migrate Contextual Search code to AOSP - Adds ContextualSearchInvoker - Adds ContextualSearchStateManager - Adds ContextualSearchHapticManager Example output from ContextualSearchStateManager in TouchInteractionService dump: https://paste.googleplex.com/5536017815961600 Bug: 353715553 Test: go/contextual-search-entrypoints-test-plan Flag: EXEMPT moving code from vendor to aosp Change-Id: I8253294d6be427e0fc29d6052994de0c03f05a3e --- quickstep/res/values/config.xml | 4 +- quickstep/res/values/strings.xml | 4 + .../model/QuickstepModelDelegate.java | 3 + .../taskbar/TaskbarEduTooltipController.kt | 21 +- .../launcher3/taskbar/TaskbarManager.java | 4 +- .../taskbar/TaskbarNavButtonController.java | 13 +- .../taskbar/TaskbarViewCallbacks.java | 4 +- .../taskbar/TaskbarViewCallbacksFactory.kt | 34 ++- .../TaskbarAllAppsButtonContainer.kt | 11 +- .../RecentsAnimationDeviceState.java | 11 +- .../com/android/quickstep/SystemUiProxy.java | 6 +- .../quickstep/TouchInteractionService.java | 11 +- .../dagger/QuickstepBaseAppComponent.java | 3 + .../NavHandleLongPressHandler.java | 139 ++++++++- .../NavHandleLongPressInputConsumer.java | 10 +- .../quickstep/util/AssistStateManager.java | 93 ------ .../android/quickstep/util/AssistUtils.java | 45 --- .../util/ContextualSearchHapticManager.kt | 101 +++++++ .../quickstep/util/ContextualSearchInvoker.kt | 224 ++++++++++++++ .../util/ContextualSearchStateManager.java | 281 ++++++++++++++++++ .../TaskbarNavButtonControllerTest.java | 22 +- .../NavHandleLongPressHandlerTest.java | 79 +++++ .../util/ContextualSearchInvokerTest.java | 253 ++++++++++++++++ .../launcher3/logging/StatsLogManager.java | 42 +++ 24 files changed, 1223 insertions(+), 195 deletions(-) delete mode 100644 quickstep/src/com/android/quickstep/util/AssistStateManager.java delete mode 100644 quickstep/src/com/android/quickstep/util/AssistUtils.java create mode 100644 quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt create mode 100644 quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt create mode 100644 quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml index e8cb5d5241..41b2384a35 100644 --- a/quickstep/res/values/config.xml +++ b/quickstep/res/values/config.xml @@ -37,8 +37,8 @@ com.android.launcher3.taskbar.TaskbarEduTooltipController com.android.quickstep.contextualeducation.SystemContextualEduStatsManager - - + + com.android.launcher3.uioverrides.SystemApiWrapper collapse %1$s + + + Circle to Search diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java index 4014f069cc..29e1f4e409 100644 --- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java +++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java @@ -77,6 +77,7 @@ import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PersistedItemArray; import com.android.quickstep.logging.SettingsChangeLogger; import com.android.quickstep.logging.StatsLogCompatManager; +import com.android.quickstep.util.ContextualSearchStateManager; import com.android.systemui.shared.system.SysUiStatsLog; import java.util.ArrayList; @@ -209,6 +210,8 @@ public class QuickstepModelDelegate extends ModelDelegate { @Override public void workspaceLoadComplete() { super.workspaceLoadComplete(); + // Initialize ContextualSearchStateManager. + ContextualSearchStateManager.INSTANCE.get(mContext); recreatePredictors(); } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt index 06376d3fd2..ade8f8cdc6 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt @@ -49,6 +49,7 @@ import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN import com.android.launcher3.util.ResourceBasedOverride import com.android.launcher3.views.ActivityContext import com.android.launcher3.views.BaseDragLayer +import com.android.quickstep.util.ContextualSearchInvoker import com.android.quickstep.util.LottieAnimationColorUtils import java.io.PrintWriter @@ -80,7 +81,11 @@ open class TaskbarEduTooltipController(context: Context) : ResourceBasedOverride, LoggableTaskbarController { protected val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context) - open val shouldShowSearchEdu = false + open val shouldShowSearchEdu: Boolean + get() = + ContextualSearchInvoker.newInstance(activityContext) + .runContextualSearchInvocationChecksAndLogFailures() + private val isTooltipEnabled: Boolean get() { return !Utilities.isRunningInTestHarness() && @@ -351,19 +356,19 @@ open class TaskbarEduTooltipController(context: Context) : overlayContext.layoutInflater.inflate( R.layout.taskbar_edu_tooltip, overlayContext.dragLayer, - false + false, ) as TaskbarEduTooltip controllers.taskbarAutohideSuspendController.updateFlag( FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, - true + true, ) tooltip.onCloseCallback = { this.tooltip = null controllers.taskbarAutohideSuspendController.updateFlag( FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, - false + false, ) controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true) } @@ -378,7 +383,7 @@ open class TaskbarEduTooltipController(context: Context) : override fun performAccessibilityAction( host: View, action: Int, - args: Bundle? + args: Bundle?, ): Boolean { if (action == R.id.close) { hide() @@ -396,13 +401,13 @@ open class TaskbarEduTooltipController(context: Context) : override fun onInitializeAccessibilityNodeInfo( host: View, - info: AccessibilityNodeInfo + info: AccessibilityNodeInfo, ) { super.onInitializeAccessibilityNodeInfo(host, info) info.addAction( AccessibilityNodeInfo.AccessibilityAction( R.id.close, - host.context?.getText(R.string.taskbar_edu_close) + host.context?.getText(R.string.taskbar_edu_close), ) ) } @@ -421,7 +426,7 @@ open class TaskbarEduTooltipController(context: Context) : return ResourceBasedOverride.Overrides.getObject( TaskbarEduTooltipController::class.java, context, - R.string.taskbar_edu_tooltip_controller_class + R.string.taskbar_edu_tooltip_controller_class, ) } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java index 78e7b470ff..f9a84a75be 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java @@ -72,7 +72,7 @@ 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.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; import com.android.systemui.shared.statusbar.phone.BarTransitions; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; @@ -250,7 +250,7 @@ public class TaskbarManager { SystemUiProxy.INSTANCE.get(mContext), ContextualEduStatsManager.INSTANCE.get(mContext), new Handler(), - AssistUtils.newInstance(mContext)); + ContextualSearchInvoker.newInstance(mContext)); mComponentCallbacks = new ComponentCallbacks() { private Configuration mOldConfig = mContext.getResources().getConfiguration(); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java index 15c35b644e..894791479e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java @@ -51,7 +51,7 @@ import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; @@ -113,7 +113,7 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa private final SystemUiProxy mSystemUiProxy; private final ContextualEduStatsManager mContextualEduStatsManager; private final Handler mHandler; - private final AssistUtils mAssistUtils; + private final ContextualSearchInvoker mContextualSearchInvoker; @Nullable private StatsLogManager mStatsLogManager; private final Runnable mResetLongPress = this::resetScreenUnpin; @@ -124,13 +124,13 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa SystemUiProxy systemUiProxy, ContextualEduStatsManager contextualEduStatsManager, Handler handler, - AssistUtils assistUtils) { + ContextualSearchInvoker contextualSearchInvoker) { mContext = context; mCallbacks = callbacks; mSystemUiProxy = systemUiProxy; mContextualEduStatsManager = contextualEduStatsManager; mHandler = handler; - mAssistUtils = assistUtils; + mContextualSearchInvoker = contextualSearchInvoker; } public void onButtonClick(@TaskbarButton int buttonType, View view) { @@ -344,8 +344,9 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa if (mScreenPinned || !mAssistantLongPressEnabled) { return; } - // Attempt to start Assist with AssistUtils, otherwise fall back to SysUi's implementation. - if (!mAssistUtils.tryStartAssistOverride(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) { + // Attempt to start Contextual Search, otherwise fall back to SysUi's implementation. + if (!mContextualSearchInvoker.tryStartAssistOverride( + INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) { Bundle args = new Bundle(); args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); mSystemUiProxy.startAssistant(args); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java index 176be1ce5b..8bc1e12395 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java @@ -19,6 +19,7 @@ package com.android.launcher3.taskbar; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP; +import android.content.Context; import android.view.InputDevice; import android.view.MotionEvent; import android.view.View; @@ -64,7 +65,8 @@ public class TaskbarViewCallbacks { mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS); } - public boolean isAllAppsButtonHapticFeedbackEnabled() { + /** @return true if haptic feedback should occur when long pressing the all apps button. */ + public boolean isAllAppsButtonHapticFeedbackEnabled(Context context) { return false; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt index ba0f5a01b0..704d6cf6c1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt @@ -16,10 +16,14 @@ package com.android.launcher3.taskbar +import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_META import android.content.Context import com.android.launcher3.R +import com.android.launcher3.logging.StatsLogManager import com.android.launcher3.util.ResourceBasedOverride import com.android.launcher3.util.ResourceBasedOverride.Overrides +import com.android.quickstep.TopTaskTracker +import com.android.quickstep.util.ContextualSearchInvoker /** Creates [TaskbarViewCallbacks] instances. */ open class TaskbarViewCallbacksFactory : ResourceBasedOverride { @@ -28,7 +32,35 @@ open class TaskbarViewCallbacksFactory : ResourceBasedOverride { activity: TaskbarActivityContext, controllers: TaskbarControllers, taskbarView: TaskbarView, - ): TaskbarViewCallbacks = TaskbarViewCallbacks(activity, controllers, taskbarView) + ): TaskbarViewCallbacks { + return object : TaskbarViewCallbacks(activity, controllers, taskbarView) { + override fun triggerAllAppsButtonLongClick() { + super.triggerAllAppsButtonLongClick() + + val contextualSearchInvoked = + ContextualSearchInvoker.newInstance(activity).show(ENTRYPOINT_LONG_PRESS_META) + if (contextualSearchInvoked) { + val runningPackage = + TopTaskTracker.INSTANCE[activity].getCachedTopTask( + /* filterOnlyVisibleRecents */ true + ) + .getPackageName() + activity.statsLogManager + .logger() + .withPackageName(runningPackage) + .log(StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_META) + } + } + + override fun isAllAppsButtonHapticFeedbackEnabled(context: Context): Boolean { + return longPressAllAppsToStartContextualSearch(context) + } + } + } + + open fun longPressAllAppsToStartContextualSearch(context: Context): Boolean = + ContextualSearchInvoker.newInstance(context) + .runContextualSearchInvocationChecksAndLogFailures() companion object { @JvmStatic diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt index e6c0b2f087..c5f8aa091d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt @@ -37,7 +37,7 @@ import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.launcher3.views.ActivityContext import com.android.launcher3.views.IconButtonView import com.android.quickstep.DeviceConfigWrapper -import com.android.quickstep.util.AssistStateManager +import com.android.quickstep.util.ContextualSearchStateManager /** Taskbar all apps button container for customizable taskbar. */ class TaskbarAllAppsButtonContainer @@ -79,17 +79,18 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 setOnClickListener(this::onAllAppsButtonClick) setOnLongClickListener(this::onAllAppsButtonLongClick) setOnTouchListener(this::onAllAppsButtonTouch) - isHapticFeedbackEnabled = taskbarViewCallbacks.isAllAppsButtonHapticFeedbackEnabled() + isHapticFeedbackEnabled = + taskbarViewCallbacks.isAllAppsButtonHapticFeedbackEnabled(mContext) allAppsTouchRunnable = Runnable { taskbarViewCallbacks.triggerAllAppsButtonLongClick() allAppsTouchTriggered = true } - val assistStateManager = AssistStateManager.INSTANCE[mContext] + val contextualSearchStateManager = ContextualSearchStateManager.INSTANCE[mContext] if ( DeviceConfigWrapper.get().customLpaaThresholds && - assistStateManager.lpnhDurationMillis.isPresent + contextualSearchStateManager.lpnhDurationMillis.isPresent ) { - allAppsButtonTouchDelayMs = assistStateManager.lpnhDurationMillis.get() + allAppsButtonTouchDelayMs = contextualSearchStateManager.lpnhDurationMillis.get() } } diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java index 51317743cd..2fa201d034 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java @@ -70,7 +70,7 @@ import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.SettingsCache; import com.android.quickstep.TopTaskTracker.CachedTaskInfo; import com.android.quickstep.util.ActiveGestureLog; -import com.android.quickstep.util.AssistStateManager; +import com.android.quickstep.util.ContextualSearchStateManager; import com.android.quickstep.util.GestureExclusionManager; import com.android.quickstep.util.GestureExclusionManager.ExclusionListener; import com.android.quickstep.util.NavBarPosition; @@ -101,7 +101,7 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, E private final DisplayController mDisplayController; private final GestureExclusionManager mExclusionManager; - private final AssistStateManager mAssistStateManager; + private final ContextualSearchStateManager mContextualSearchStateManager; private final RotationTouchHelper mRotationTouchHelper; private final TaskStackChangeListener mPipListener; @@ -152,7 +152,7 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, E mContext = context; mDisplayController = DisplayController.INSTANCE.get(context); mExclusionManager = exclusionManager; - mAssistStateManager = AssistStateManager.INSTANCE.get(context); + mContextualSearchStateManager = ContextualSearchStateManager.INSTANCE.get(context); mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false); mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(context); if (isInstanceForTouches) { @@ -617,8 +617,9 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, E : QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON; float touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); - if (mAssistStateManager.getLPNHCustomSlopMultiplier().isPresent()) { - float customSlopMultiplier = mAssistStateManager.getLPNHCustomSlopMultiplier().get(); + if (mContextualSearchStateManager.getLPNHCustomSlopMultiplier().isPresent()) { + float customSlopMultiplier = + mContextualSearchStateManager.getLPNHCustomSlopMultiplier().get(); return customSlopMultiplier * slopMultiplier * touchSlop; } else { return slopMultiplier * touchSlop; diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index 5f028937b0..eeea528c60 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -64,7 +64,7 @@ import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.SafeCloseable; import com.android.quickstep.util.ActiveGestureProtoLogProxy; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -311,8 +311,8 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable { setBackToLauncherCallback(mBackToLauncherCallback, mBackToLauncherRunner); setUnfoldAnimationListener(mUnfoldAnimationListener); setDesktopTaskListener(mDesktopTaskListener); - setAssistantOverridesRequested( - AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes()); + setAssistantOverridesRequested(ContextualSearchInvoker.newInstance(mContext) + .getSysUiAssistOverrideInvocationTypes()); mStateChangeCallbacks.forEach(Runnable::run); if (mUnfoldTransitionProvider != null) { diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index f0943dc798..41a8a3150f 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -122,8 +122,8 @@ import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer; import com.android.quickstep.util.ActiveGestureLog; import com.android.quickstep.util.ActiveGestureLog.CompoundString; import com.android.quickstep.util.ActiveGestureProtoLogProxy; -import com.android.quickstep.util.AssistStateManager; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; +import com.android.quickstep.util.ContextualSearchStateManager; import com.android.quickstep.views.RecentsViewContainer; import com.android.systemui.shared.recents.IOverviewProxy; import com.android.systemui.shared.recents.ISystemUiProxy; @@ -297,7 +297,8 @@ public class TouchInteractionService extends Service { @Override public void onAssistantOverrideInvoked(int invocationType) { executeForTouchInteractionService(tis -> { - if (!AssistUtils.newInstance(tis).tryStartAssistOverride(invocationType)) { + if (!ContextualSearchInvoker.newInstance(tis) + .tryStartAssistOverride(invocationType)) { Log.w(TAG, "Failed to invoke Assist override"); } }); @@ -1640,8 +1641,8 @@ public class TouchInteractionService extends Service { } mTaskbarManager.dumpLogs("", pw); mDesktopVisibilityController.dumpLogs("", pw); - pw.println("AssistStateManager:"); - AssistStateManager.INSTANCE.get(this).dump("\t", pw); + pw.println("ContextualSearchStateManager:"); + ContextualSearchStateManager.INSTANCE.get(this).dump("\t", pw); SystemUiProxy.INSTANCE.get(this).dump(pw); DeviceConfigWrapper.get().dump(" ", pw); } diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java index 341c86892a..335161b24c 100644 --- a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java +++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java @@ -21,6 +21,7 @@ import com.android.launcher3.dagger.LauncherBaseAppComponent; import com.android.launcher3.model.WellbeingModel; import com.android.quickstep.logging.SettingsChangeLogger; import com.android.quickstep.util.AsyncClockEventDelegate; +import com.android.quickstep.util.ContextualSearchHapticManager; /** * Launcher Quickstep base component for Dagger injection. @@ -36,4 +37,6 @@ public interface QuickstepBaseAppComponent extends LauncherBaseAppComponent { WellbeingModel getWellbeingModel(); AsyncClockEventDelegate getAsyncClockEventDelegate(); + + ContextualSearchHapticManager getContextualSearchHapticManager(); } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java index 1d00e533f5..1a825a42e3 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java @@ -16,25 +16,67 @@ package com.android.quickstep.inputconsumers; +import static android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_NAV_HANDLE; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_SUCCESSFUL_NAV_HANDLE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OMNI_GET_LONG_PRESS_RUNNABLE; +import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_OMNI_RUNNABLE; + import android.content.Context; +import android.os.SystemClock; +import android.util.Log; +import android.view.ViewConfiguration; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.android.launcher3.LauncherApplication; import com.android.launcher3.R; +import com.android.launcher3.logging.InstanceId; +import com.android.launcher3.logging.InstanceIdSequence; +import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.util.ResourceBasedOverride; +import com.android.launcher3.util.VibratorWrapper; +import com.android.quickstep.DeviceConfigWrapper; import com.android.quickstep.NavHandle; +import com.android.quickstep.TopTaskTracker; +import com.android.quickstep.util.ContextualSearchHapticManager; +import com.android.quickstep.util.ContextualSearchInvoker; +import com.android.quickstep.util.ContextualSearchStateManager; /** * Class for extending nav handle long press behavior */ public class NavHandleLongPressHandler implements ResourceBasedOverride { + private static final String TAG = "NavHandleLongPressHandler"; + + protected final Context mContext; + protected final VibratorWrapper mVibratorWrapper; + protected final ContextualSearchHapticManager mContextualSearchHapticManager; + protected final ContextualSearchInvoker mContextualSearchInvoker; + protected final StatsLogManager mStatsLogManager; + private boolean mPendingInvocation; + + public NavHandleLongPressHandler(Context context) { + mContext = context; + mStatsLogManager = StatsLogManager.newInstance(context); + mVibratorWrapper = VibratorWrapper.INSTANCE.get(mContext); + mContextualSearchHapticManager = ((LauncherApplication) context.getApplicationContext()) + .getAppComponent().getContextualSearchHapticManager(); + mContextualSearchInvoker = ContextualSearchInvoker.newInstance(mContext); + } + /** Creates NavHandleLongPressHandler as specified by overrides */ public static NavHandleLongPressHandler newInstance(Context context) { return Overrides.getObject(NavHandleLongPressHandler.class, context, R.string.nav_handle_long_press_handler_class); } + protected boolean isContextualSearchEntrypointEnabled(NavHandle navHandle) { + return DeviceConfigWrapper.get().getEnableLongPressNavHandle(); + } + /** * Called when nav handle is long pressed to get the Runnable that should be executed by the * caller to invoke long press behavior. If null is returned that means long press couldn't be @@ -46,8 +88,48 @@ public class NavHandleLongPressHandler implements ResourceBasedOverride { * * @param navHandle to handle this long press */ - public @Nullable Runnable getLongPressRunnable(NavHandle navHandle) { - return null; + @Nullable + @VisibleForTesting + final Runnable getLongPressRunnable(NavHandle navHandle) { + if (!isContextualSearchEntrypointEnabled(navHandle)) { + Log.i(TAG, "Contextual Search invocation failed: entry point disabled"); + mVibratorWrapper.cancelVibrate(); + return null; + } + + if (!mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()) { + Log.i(TAG, "Contextual Search invocation failed: precondition not satisfied"); + mVibratorWrapper.cancelVibrate(); + return null; + } + + mPendingInvocation = true; + Log.i(TAG, "Contextual Search invocation: invocation runnable created"); + InstanceId instanceId = new InstanceIdSequence().newInstanceId(); + mStatsLogManager.logger().withInstanceId(instanceId).log( + LAUNCHER_OMNI_GET_LONG_PRESS_RUNNABLE); + long startTimeMillis = SystemClock.elapsedRealtime(); + return () -> { + mStatsLogManager.latencyLogger().withInstanceId(instanceId).withLatency( + SystemClock.elapsedRealtime() - startTimeMillis).log( + LAUNCHER_LATENCY_OMNI_RUNNABLE); + if (mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + ENTRYPOINT_LONG_PRESS_NAV_HANDLE)) { + Log.i(TAG, "Contextual Search invocation successful"); + + String runningPackage = TopTaskTracker.INSTANCE.get(mContext).getCachedTopTask( + /* filterOnlyVisibleRecents */ true).getPackageName(); + mStatsLogManager.logger().withPackageName(runningPackage) + .log(LAUNCHER_LAUNCH_ASSISTANT_SUCCESSFUL_NAV_HANDLE); + } else { + mVibratorWrapper.cancelVibrate(); + if (DeviceConfigWrapper.get().getAnimateLpnh() + && !DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) { + navHandle.animateNavBarLongPress( + /*isTouchDown*/false, /*shrink*/ false, /*durationMs*/160); + } + } + }; } /** @@ -55,7 +137,15 @@ public class NavHandleLongPressHandler implements ResourceBasedOverride { * * @param navHandle to handle the animation for this touch */ - public void onTouchStarted(NavHandle navHandle) {} + @VisibleForTesting + final void onTouchStarted(NavHandle navHandle) { + mPendingInvocation = false; + if (isContextualSearchEntrypointEnabled(navHandle) + && mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()) { + Log.i(TAG, "Contextual Search invocation: touch started"); + startNavBarAnimation(navHandle); + } + } /** * Called when nav handle gesture is finished by the user lifting their finger or the system @@ -64,5 +154,46 @@ public class NavHandleLongPressHandler implements ResourceBasedOverride { * @param navHandle to handle the animation for this touch * @param reason why the touch ended */ - public void onTouchFinished(NavHandle navHandle, String reason) {} + @VisibleForTesting + final void onTouchFinished(NavHandle navHandle, String reason) { + Log.i(TAG, "Contextual Search invocation: touch finished with reason: " + reason); + + if (!DeviceConfigWrapper.get().getShrinkNavHandleOnPress() || !mPendingInvocation) { + mVibratorWrapper.cancelVibrate(); + } + + if (DeviceConfigWrapper.get().getAnimateLpnh()) { + if (DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) { + navHandle.animateNavBarLongPress( + /*isTouchDown*/false, /*shrink*/ true, /*durationMs*/200); + } else { + navHandle.animateNavBarLongPress( + /*isTouchDown*/false, /*shrink*/ false, /*durationMs*/ 160); + } + } + } + + @VisibleForTesting + final void startNavBarAnimation(NavHandle navHandle) { + mContextualSearchHapticManager.vibrateForSearchHint(); + + if (DeviceConfigWrapper.get().getAnimateLpnh()) { + if (DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) { + navHandle.animateNavBarLongPress( + /*isTouchDown*/ true, /*shrink*/true, /*durationMs*/200); + } else { + long longPressTimeout; + ContextualSearchStateManager contextualSearchStateManager = + ContextualSearchStateManager.INSTANCE.get(mContext); + if (contextualSearchStateManager.getLPNHDurationMillis().isPresent()) { + longPressTimeout = + contextualSearchStateManager.getLPNHDurationMillis().get().intValue(); + } else { + longPressTimeout = ViewConfiguration.getLongPressTimeout(); + } + navHandle.animateNavBarLongPress( + /*isTouchDown*/ true, /*shrink*/ false, /*durationMs*/ longPressTimeout); + } + } + } } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java index f4d36953b5..f5bef05e14 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java @@ -38,7 +38,7 @@ import com.android.quickstep.InputConsumer; import com.android.quickstep.NavHandle; import com.android.quickstep.RecentsAnimationDeviceState; import com.android.quickstep.TopTaskTracker; -import com.android.quickstep.util.AssistStateManager; +import com.android.quickstep.util.ContextualSearchStateManager; import com.android.systemui.shared.system.InputMonitorCompat; /** @@ -75,9 +75,11 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { super(delegate, inputMonitor); mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x; mDeepPressEnabled = DeviceConfigWrapper.get().getEnableLpnhDeepPress(); - AssistStateManager assistStateManager = AssistStateManager.INSTANCE.get(context); - if (assistStateManager.getLPNHDurationMillis().isPresent()) { - mLongPressTimeout = assistStateManager.getLPNHDurationMillis().get().intValue(); + ContextualSearchStateManager contextualSearchStateManager = + ContextualSearchStateManager.INSTANCE.get(context); + if (contextualSearchStateManager.getLPNHDurationMillis().isPresent()) { + mLongPressTimeout = + contextualSearchStateManager.getLPNHDurationMillis().get().intValue(); } else { mLongPressTimeout = ViewConfiguration.getLongPressTimeout(); } diff --git a/quickstep/src/com/android/quickstep/util/AssistStateManager.java b/quickstep/src/com/android/quickstep/util/AssistStateManager.java deleted file mode 100644 index 7acb28da11..0000000000 --- a/quickstep/src/com/android/quickstep/util/AssistStateManager.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2023 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.util; - -import static com.android.launcher3.util.MainThreadInitializedObject.forOverride; - -import com.android.launcher3.R; -import com.android.launcher3.util.MainThreadInitializedObject; -import com.android.launcher3.util.ResourceBasedOverride; -import com.android.launcher3.util.SafeCloseable; - -import java.io.PrintWriter; -import java.util.Optional; - -/** Class to manage Assistant states. */ -public class AssistStateManager implements ResourceBasedOverride, SafeCloseable { - - public static final MainThreadInitializedObject INSTANCE = - forOverride(AssistStateManager.class, R.string.assist_state_manager_class); - - public AssistStateManager() {} - - /** Return {@code true} if the Settings toggle is enabled. */ - public boolean isSettingsAllEntrypointsEnabled() { - return false; - } - - /** Whether search supports showing on the lockscreen. */ - public boolean supportsShowWhenLocked() { - return false; - } - - /** Whether ContextualSearchService invocation path is available. */ - public boolean isContextualSearchServiceAvailable() { - return false; - } - - /** Get the Launcher overridden long press nav handle duration to trigger Assistant. */ - public Optional getLPNHDurationMillis() { - return Optional.empty(); - } - - /** - * Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant. - */ - public Optional getLPNHCustomSlopMultiplier() { - return Optional.empty(); - } - - /** Get the Launcher overridden long press home duration to trigger Assistant. */ - public Optional getLPHDurationMillis() { - return Optional.empty(); - } - - /** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */ - public Optional getLPHCustomSlopMultiplier() { - return Optional.empty(); - } - - /** Get the long press duration data source. */ - public int getDurationDataSource() { - return 0; - } - - /** Get the long press touch slop multiplier data source. */ - public int getSlopDataSource() { - return 0; - } - - /** Get the haptic bit overridden by AGSA. */ - public Optional getShouldPlayHapticOverride() { - return Optional.empty(); - } - - /** Dump states. */ - public void dump(String prefix, PrintWriter writer) {} - - @Override - public void close() {} -} diff --git a/quickstep/src/com/android/quickstep/util/AssistUtils.java b/quickstep/src/com/android/quickstep/util/AssistUtils.java deleted file mode 100644 index 11b6ea7d21..0000000000 --- a/quickstep/src/com/android/quickstep/util/AssistUtils.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2023 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.util; - -import android.content.Context; - -import com.android.launcher3.R; -import com.android.launcher3.util.ResourceBasedOverride; - -/** Utilities to work with Assistant functionality. */ -public class AssistUtils implements ResourceBasedOverride { - - public AssistUtils() {} - - /** Creates AssistUtils as specified by overrides */ - public static AssistUtils newInstance(Context context) { - return Overrides.getObject(AssistUtils.class, context, R.string.assist_utils_class); - } - - /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */ - public int[] getSysUiAssistOverrideInvocationTypes() { - return new int[0]; - } - - /** - * @return {@code true} if the override was handled, i.e. an assist surface was shown or the - * request should be ignored. {@code false} means the caller should start assist another way. - */ - public boolean tryStartAssistOverride(int invocationType) { - return false; - } -} diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt new file mode 100644 index 0000000000..8c246a5240 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt @@ -0,0 +1,101 @@ +/* + * 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.util + +import android.content.Context +import android.os.VibrationEffect +import android.os.VibrationEffect.Composition +import android.os.Vibrator +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.util.VibratorWrapper +import com.android.quickstep.DeviceConfigWrapper.Companion.get +import javax.inject.Inject +import kotlin.math.pow + +/** Manages haptics relating to Contextual Search invocations. */ +@LauncherAppSingleton +class ContextualSearchHapticManager +@Inject +internal constructor(@ApplicationContext private val context: Context) { + + private var searchEffect = createSearchEffect() + private var contextualSearchStateManager = ContextualSearchStateManager.INSTANCE[context] + + private fun createSearchEffect() = + if ( + context + .getSystemService(Vibrator::class.java)!! + .areAllPrimitivesSupported(Composition.PRIMITIVE_TICK) + ) { + VibrationEffect.startComposition() + .addPrimitive(Composition.PRIMITIVE_TICK, 1f) + .compose() + } else { + // fallback for devices without composition support + VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK) + } + + /** Indicates that search has been invoked. */ + fun vibrateForSearch() { + searchEffect.let { VibratorWrapper.INSTANCE[context].vibrate(it) } + } + + /** Indicates that search will be invoked if the current gesture is maintained. */ + fun vibrateForSearchHint() { + val navbarConfig = get() + // Whether we should play the hint (ramp up) haptic + val shouldVibrate: Boolean = + if ( + context + .getSystemService(Vibrator::class.java)!! + .areAllPrimitivesSupported(Composition.PRIMITIVE_LOW_TICK) + ) { + if (contextualSearchStateManager.shouldPlayHapticOverride.isPresent) { + contextualSearchStateManager.shouldPlayHapticOverride.get() + } else { + navbarConfig.enableSearchHapticHint + } + } else { + false + } + + if (shouldVibrate) { + val startScale = navbarConfig.lpnhHapticHintStartScalePercent / 100f + val endScale = navbarConfig.lpnhHapticHintEndScalePercent / 100f + val scaleExponent = navbarConfig.lpnhHapticHintScaleExponent + val iterations = navbarConfig.lpnhHapticHintIterations + val delayMs = navbarConfig.lpnhHapticHintDelay + val composition = VibrationEffect.startComposition() + for (i in 0 until iterations) { + val t = i / (iterations - 1f) + val scale = + ((1 - t) * startScale + t * endScale) + .toDouble() + .pow(scaleExponent.toDouble()) + .toFloat() + if (i == 0) { + // Adds a delay before the ramp starts + composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale, delayMs) + } else { + composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale) + } + } + VibratorWrapper.INSTANCE[context].vibrate(composition.compose()) + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt new file mode 100644 index 0000000000..dcb72aa4f3 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt @@ -0,0 +1,224 @@ +/* + * 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.util + +import android.app.contextualsearch.ContextualSearchManager +import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_HOME +import android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH +import android.content.Context +import android.util.Log +import com.android.internal.app.AssistUtils +import com.android.launcher3.LauncherApplication +import com.android.launcher3.R +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME +import com.android.launcher3.util.ResourceBasedOverride +import com.android.quickstep.DeviceConfigWrapper +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.TopTaskTracker +import com.android.systemui.shared.system.QuickStepContract + +/** Handles invocations and checks for Contextual Search. */ +open class ContextualSearchInvoker +internal constructor( + protected val context: Context, + private val contextualSearchStateManager: ContextualSearchStateManager, + private val topTaskTracker: TopTaskTracker, + private val systemUiProxy: SystemUiProxy, + protected val statsLogManager: StatsLogManager, + private val contextualSearchHapticManager: ContextualSearchHapticManager, + private val contextualSearchManager: ContextualSearchManager?, +) : ResourceBasedOverride { + constructor( + context: Context + ) : this( + context, + ContextualSearchStateManager.INSTANCE[context], + TopTaskTracker.INSTANCE[context], + SystemUiProxy.INSTANCE[context], + StatsLogManager.newInstance(context), + (context.applicationContext as LauncherApplication) + .appComponent + .contextualSearchHapticManager, + context.getSystemService(ContextualSearchManager::class.java), + ) + + /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */ + open fun getSysUiAssistOverrideInvocationTypes(): IntArray { + val overrideInvocationTypes = com.android.launcher3.util.IntArray() + if (context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { + overrideInvocationTypes.add(AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) + } + return overrideInvocationTypes.toArray() + } + + /** + * @return `true` if the override was handled, i.e. an assist surface was shown or the request + * should be ignored. `false` means the caller should start assist another way. + */ + fun tryStartAssistOverride(invocationType: Int): Boolean { + if (invocationType == AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) { + if (!context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { + // When Contextual Search is disabled, fall back to Assistant. + return false + } + + val success = show(ENTRYPOINT_LONG_PRESS_HOME) + if (success) { + val runningPackage = + TopTaskTracker.INSTANCE[context].getCachedTopTask( + /* filterOnlyVisibleRecents */ true + ) + .getPackageName() + statsLogManager + .logger() + .withPackageName(runningPackage) + .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME) + } + + // Regardless of success, do not fall back to other assistant. + return true + } + return false + } + + /** + * Invoke Contextual Search via ContextualSearchService if availability checks are successful + * + * @param entryPoint one of the ENTRY_POINT_* constants defined in this class + * @return true if invocation was successful, false otherwise + */ + fun show(entryPoint: Int): Boolean { + return if (!runContextualSearchInvocationChecksAndLogFailures()) false + else invokeContextualSearchUnchecked(entryPoint) + } + + /** + * Run availability checks and log errors to WW. If successful the caller is expected to call + * {@link invokeContextualSearchUnchecked} + * + * @return true if availability checks were successful, false otherwise. + */ + fun runContextualSearchInvocationChecksAndLogFailures(): Boolean { + if ( + contextualSearchManager == null || + !context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH) + ) { + Log.i(TAG, "Contextual Search invocation failed: no ContextualSearchManager") + statsLogManager.logger().log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR) + return false + } + if (!contextualSearchStateManager.isContextualSearchSettingEnabled) { + Log.i(TAG, "Contextual Search invocation failed: setting disabled") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED) + return false + } + if (isNotificationShadeShowing()) { + Log.i(TAG, "Contextual Search invocation failed: notification shade") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE) + return false + } + if (isKeyguardShowing()) { + Log.i(TAG, "Contextual Search invocation attempted: keyguard") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD) + if (!contextualSearchStateManager.isInvocationAllowedOnKeyguard) { + Log.i(TAG, "Contextual Search invocation failed: keyguard not allowed") + return false + } else if (!contextualSearchStateManager.supportsShowWhenLocked()) { + Log.i(TAG, "Contextual Search invocation failed: AGA doesn't support keyguard") + return false + } + } + if (isInSplitscreen()) { + Log.i(TAG, "Contextual Search invocation attempted: splitscreen") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN) + if (!contextualSearchStateManager.isInvocationAllowedInSplitscreen) { + Log.i(TAG, "Contextual Search invocation failed: splitscreen not allowed") + return false + } + } + if (!contextualSearchStateManager.isContextualSearchIntentAvailable) { + Log.i(TAG, "Contextual Search invocation failed: no matching CSS intent filter") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE) + return false + } + + return true + } + + /** + * Invoke Contextual Search via ContextualSearchService and do haptic + * + * @param entryPoint Entry point identifier, passed to ContextualSearchService. + * @return true if invocation was successful, false otherwise + */ + fun invokeContextualSearchUncheckedWithHaptic(entryPoint: Int): Boolean { + return invokeContextualSearchUnchecked(entryPoint, withHaptic = true) + } + + private fun invokeContextualSearchUnchecked( + entryPoint: Int, + withHaptic: Boolean = false, + ): Boolean { + if (withHaptic && DeviceConfigWrapper.get().enableSearchHapticCommit) { + contextualSearchHapticManager.vibrateForSearch() + } + if (contextualSearchManager == null) { + return false + } + contextualSearchManager.startContextualSearch(entryPoint) + return true + } + + private fun isInSplitscreen(): Boolean { + return topTaskTracker.getRunningSplitTaskIds().isNotEmpty() + } + + private fun isNotificationShadeShowing(): Boolean { + return systemUiProxy.lastSystemUiStateFlags and SHADE_EXPANDED_SYSUI_FLAGS != 0L + } + + private fun isKeyguardShowing(): Boolean { + return systemUiProxy.lastSystemUiStateFlags and KEYGUARD_SHOWING_SYSUI_FLAGS != 0L + } + + companion object { + private const val TAG = "ContextualSearchInvoker" + const val SHADE_EXPANDED_SYSUI_FLAGS = + QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED or + QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED + const val KEYGUARD_SHOWING_SYSUI_FLAGS = + (QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING or + QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING or + QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) + + @JvmStatic + fun newInstance(context: Context): ContextualSearchInvoker { + return ResourceBasedOverride.Overrides.getObject( + ContextualSearchInvoker::class.java, + context, + R.string.contextual_search_invoker_class, + ) + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java new file mode 100644 index 0000000000..142fc5843d --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2023 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.util; + +import static android.app.contextualsearch.ContextualSearchManager.ACTION_LAUNCH_CONTEXTUAL_SEARCH; +import static android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_SYSTEM_ACTION; +import static android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import static com.android.launcher3.util.MainThreadInitializedObject.forOverride; +import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_SEARCH_SCREEN; + +import android.annotation.Nullable; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.content.Context; +import android.content.IIntentReceiver; +import android.content.IIntentSender; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Log; +import android.view.accessibility.AccessibilityManager; + +import androidx.annotation.CallSuper; +import androidx.annotation.VisibleForTesting; + +import com.android.launcher3.R; +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.util.EventLogArray; +import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.util.ResourceBasedOverride; +import com.android.launcher3.util.SafeCloseable; +import com.android.launcher3.util.SettingsCache; +import com.android.launcher3.util.SimpleBroadcastReceiver; +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TopTaskTracker; + +import java.io.PrintWriter; +import java.util.Optional; + +/** Long-lived class to manage Contextual Search states like the user setting and availability. */ +public class ContextualSearchStateManager implements ResourceBasedOverride, SafeCloseable { + + public static final MainThreadInitializedObject INSTANCE = + forOverride(ContextualSearchStateManager.class, + R.string.contextual_search_state_manager_class); + + private static final String TAG = "ContextualSearchStMgr"; + private static final int MAX_DEBUG_EVENT_SIZE = 20; + private static final Uri SEARCH_ALL_ENTRYPOINTS_ENABLED_URI = + Settings.Secure.getUriFor(Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED); + + private final Runnable mSysUiStateChangeListener = this::updateOverridesToSysUi; + private final SimpleBroadcastReceiver mContextualSearchPackageReceiver = + new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, (unused) -> requestUpdateProperties()); + private final SettingsCache.OnChangeListener mContextualSearchSettingChangedListener = + this::onContextualSearchSettingChanged; + protected final EventLogArray mEventLogArray = new EventLogArray(TAG, MAX_DEBUG_EVENT_SIZE); + + @Nullable private SettingsCache mSettingsCache; + // Cached value whether the ContextualSearch intent filter matched any enabled components. + private boolean mIsContextualSearchIntentAvailable; + private boolean mIsContextualSearchSettingEnabled; + + protected Context mContext; + protected String mContextualSearchPackage; + + public ContextualSearchStateManager() {} + + public ContextualSearchStateManager(Context context) { + mContext = context; + mContextualSearchPackage = mContext.getResources().getString( + com.android.internal.R.string.config_defaultContextualSearchPackageName); + + if (areAllContextualSearchFlagsDisabled() + || !context.getPackageManager().hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { + // If we had previously registered a SystemAction which is no longer valid, we need to + // unregister it here. + unregisterSearchScreenSystemAction(); + // Don't listen for stuff we aren't gonna use. + return; + } + + requestUpdateProperties(); + registerSearchScreenSystemAction(); + mContextualSearchPackageReceiver.registerPkgActions( + context, mContextualSearchPackage, Intent.ACTION_PACKAGE_ADDED, + Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED); + + mSettingsCache = SettingsCache.INSTANCE.get(context); + mSettingsCache.register(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI, + mContextualSearchSettingChangedListener); + onContextualSearchSettingChanged( + mSettingsCache.getValue(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI)); + SystemUiProxy.INSTANCE.get(mContext).addOnStateChangeListener(mSysUiStateChangeListener); + } + + /** Return {@code true} if the Settings toggle is enabled. */ + public final boolean isContextualSearchSettingEnabled() { + return mIsContextualSearchSettingEnabled; + } + + private void onContextualSearchSettingChanged(boolean isEnabled) { + mIsContextualSearchSettingEnabled = isEnabled; + } + + /** Whether search supports showing on the lockscreen. */ + protected boolean supportsShowWhenLocked() { + return false; + } + + /** Whether ContextualSearchService invocation path is available. */ + @VisibleForTesting + protected final boolean isContextualSearchIntentAvailable() { + return mIsContextualSearchIntentAvailable; + } + + /** Get the Launcher overridden long press nav handle duration to trigger Assistant. */ + public Optional getLPNHDurationMillis() { + return Optional.empty(); + } + + /** + * Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant. + */ + public Optional getLPNHCustomSlopMultiplier() { + return Optional.empty(); + } + + /** Get the Launcher overridden long press home duration to trigger Assistant. */ + public Optional getLPHDurationMillis() { + return Optional.empty(); + } + + /** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */ + public Optional getLPHCustomSlopMultiplier() { + return Optional.empty(); + } + + /** Get the long press duration data source. */ + public int getDurationDataSource() { + return 0; + } + + /** Get the long press touch slop multiplier data source. */ + public int getSlopDataSource() { + return 0; + } + + /** Get the haptic bit overridden by AGSA. */ + public Optional getShouldPlayHapticOverride() { + return Optional.empty(); + } + + protected boolean isInvocationAllowedOnKeyguard() { + return false; + } + + protected boolean isInvocationAllowedInSplitscreen() { + return true; + } + + @CallSuper + protected boolean areAllContextualSearchFlagsDisabled() { + return !DeviceConfigWrapper.get().getEnableLongPressNavHandle(); + } + + @CallSuper + protected void requestUpdateProperties() { + UI_HELPER_EXECUTOR.execute(() -> { + // Check that Contextual Search intent filters are enabled. + Intent csIntent = new Intent(ACTION_LAUNCH_CONTEXTUAL_SEARCH).setPackage( + mContextualSearchPackage); + mIsContextualSearchIntentAvailable = + !mContext.getPackageManager().queryIntentActivities(csIntent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE).isEmpty(); + + addEventLog("Updated isContextualSearchIntentAvailable", + mIsContextualSearchIntentAvailable); + }); + } + + protected final void updateOverridesToSysUi() { + // LPH commit haptic is always enabled + SystemUiProxy.INSTANCE.get(mContext).setOverrideHomeButtonLongPress( + getLPHDurationMillis().orElse(0L), getLPHCustomSlopMultiplier().orElse(0f), true); + Log.i(TAG, "Sent LPH override to sysui: " + getLPHDurationMillis().orElse(0L) + ";" + + getLPHCustomSlopMultiplier().orElse(0f)); + } + + private void registerSearchScreenSystemAction() { + PendingIntent searchScreenPendingIntent = new PendingIntent(new IIntentSender.Stub() { + @Override + public void send(int i, Intent intent, String s, IBinder iBinder, + IIntentReceiver iIntentReceiver, String s1, Bundle bundle) + throws RemoteException { + // Delayed slightly to minimize chance of capturing the System Actions dialog. + UI_HELPER_EXECUTOR.getHandler().postDelayed( + () -> { + boolean contextualSearchInvoked = + ContextualSearchInvoker.newInstance(mContext).show( + ENTRYPOINT_SYSTEM_ACTION); + if (contextualSearchInvoked) { + String runningPackage = + TopTaskTracker.INSTANCE.get(mContext).getCachedTopTask( + /* filterOnlyVisibleRecents */ + true).getPackageName(); + StatsLogManager.newInstance(mContext).logger() + .withPackageName(runningPackage) + .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION); + } + }, 200); + } + }); + + mContext.getSystemService(AccessibilityManager.class).registerSystemAction(new RemoteAction( + Icon.createWithResource(mContext, R.drawable.ic_allapps_search), + mContext.getString(R.string.search_gesture_feature_title), + mContext.getString(R.string.search_gesture_feature_title), + searchScreenPendingIntent), + SYSTEM_ACTION_ID_SEARCH_SCREEN); + } + + private void unregisterSearchScreenSystemAction() { + mContext.getSystemService(AccessibilityManager.class).unregisterSystemAction( + SYSTEM_ACTION_ID_SEARCH_SCREEN); + } + + /** Dump states. */ + public final void dump(String prefix, PrintWriter writer) { + synchronized (mEventLogArray) { + mEventLogArray.dump(prefix, writer); + } + } + + @Override + public void close() { + mContextualSearchPackageReceiver.unregisterReceiverSafely(mContext); + unregisterSearchScreenSystemAction(); + + if (mSettingsCache != null) { + mSettingsCache.unregister(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI, + mContextualSearchSettingChangedListener); + } + SystemUiProxy.INSTANCE.get(mContext).removeOnStateChangeListener(mSysUiStateChangeListener); + } + + protected final void addEventLog(String event) { + synchronized (mEventLogArray) { + mEventLogArray.addLog(event); + } + } + + protected final void addEventLog(String event, boolean extras) { + synchronized (mEventLogArray) { + mEventLogArray.addLog(event, extras); + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java index 02d62186b1..253d9215fe 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java @@ -39,7 +39,7 @@ import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TouchInteractionService; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; import com.android.systemui.contextualeducation.GestureType; import org.junit.Before; @@ -64,7 +64,7 @@ public class TaskbarNavButtonControllerTest { @Mock Handler mockHandler; @Mock - AssistUtils mockAssistUtils; + ContextualSearchInvoker mockContextualSearchInvoker; @Mock StatsLogManager mockStatsLogManager; @Mock @@ -109,7 +109,7 @@ public class TaskbarNavButtonControllerTest { mockSystemUiProxy, mockContextualEduStatsManager, mockHandler, - mockAssistUtils); + mockContextualSearchInvoker); } @Test @@ -166,40 +166,40 @@ public class TaskbarNavButtonControllerTest { @Test public void testLongPressHome_enabled_withoutOverride() { mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, times(1)).startAssistant(any()); } @Test public void testLongPressHome_enabled_withOverride() { mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } @Test public void testLongPressHome_disabled_withoutOverride() { mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } @Test public void testLongPressHome_disabled_withOverride() { mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java new file mode 100644 index 0000000000..9018775316 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java @@ -0,0 +1,79 @@ +/* + * 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.inputconsumers; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.NavHandle; +import com.android.quickstep.util.TestExtensions; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NavHandleLongPressHandlerTest { + + private NavHandleLongPressHandler mLongPressHandler; + @Mock private NavHandle mNavHandle; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + mLongPressHandler = new NavHandleLongPressHandler(context); + } + + @Test + public void testStartNavBarAnimation_flagDisabled() { + try (AutoCloseable flag = overrideAnimateLPNHFlag(false)) { + mLongPressHandler.startNavBarAnimation(mNavHandle); + verify(mNavHandle, never()) + .animateNavBarLongPress(anyBoolean(), anyBoolean(), anyLong()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testStartNavBarAnimation_flagEnabled() { + try (AutoCloseable flag = overrideAnimateLPNHFlag(true)) { + mLongPressHandler.startNavBarAnimation(mNavHandle); + verify(mNavHandle).animateNavBarLongPress(anyBoolean(), anyBoolean(), anyLong()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private AutoCloseable overrideAnimateLPNHFlag(boolean value) { + return TestExtensions.overrideNavConfigFlag( + "ANIMATE_LPNH", value, () -> DeviceConfigWrapper.get().getAnimateLpnh()); + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java new file mode 100644 index 0000000000..543ffe61eb --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java @@ -0,0 +1,253 @@ +/* + * 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.util; + +import static android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED; +import static com.android.quickstep.util.ContextualSearchInvoker.KEYGUARD_SHOWING_SYSUI_FLAGS; +import static com.android.quickstep.util.ContextualSearchInvoker.SHADE_EXPANDED_SYSUI_FLAGS; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.contextualsearch.ContextualSearchManager; +import android.content.Context; +import android.content.pm.PackageManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.logging.StatsLogManager; +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TopTaskTracker; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Robolectric unit tests for {@link ContextualSearchInvoker} + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ContextualSearchInvokerTest { + + private static final int CONTEXTUAL_SEARCH_ENTRY_POINT = 123; + + private @Mock PackageManager mMockPackageManager; + private @Mock ContextualSearchStateManager mMockStateManager; + private @Mock TopTaskTracker mMockTopTaskTracker; + private @Mock SystemUiProxy mMockSystemUiProxy; + private @Mock StatsLogManager mMockStatsLogManager; + private @Mock StatsLogManager.StatsLogger mMockStatsLogger; + private @Mock ContextualSearchHapticManager mMockContextualSearchHapticManager; + private @Mock ContextualSearchManager mMockContextualSearchManager; + private ContextualSearchInvoker mContextualSearchInvoker; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockPackageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)).thenReturn(true); + Context context = spy(getApplicationContext()); + doReturn(mMockPackageManager).when(context).getPackageManager(); + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(0L); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{}); + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(true); + when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(true); + when(mMockStatsLogManager.logger()).thenReturn(mMockStatsLogger); + + mContextualSearchInvoker = new ContextualSearchInvoker(context, mMockStateManager, + mMockTopTaskTracker, mMockSystemUiProxy, mMockStatsLogManager, + mMockContextualSearchHapticManager, mMockContextualSearchManager); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchFeatureIsNotAvailable() { + when(mMockPackageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)).thenReturn(false); + + assertFalse("Expected invocation to fail when feature is unavailable", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchIntentIsAvailable() { + assertTrue("Expected invocation checks to succeed", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verifyNoMoreInteractions(mMockStatsLogManager); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchIntentIsNotAvailable() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + assertFalse("Expected invocation to fail when feature is unavailable", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_settingDisabled() { + when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(false); + + assertFalse("Expected invocation checks to fail when setting is disabled", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_notificationShadeIsShowing() { + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(SHADE_EXPANDED_SYSUI_FLAGS); + + assertFalse("Expected invocation checks to fail when notification shade is showing", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_keyguardIsShowing() { + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn( + KEYGUARD_SHOWING_SYSUI_FLAGS); + + assertFalse("Expected invocation checks to fail when keyguard is showing", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_isInSplitScreen_disallowed() { + when(mMockStateManager.isInvocationAllowedInSplitscreen()).thenReturn(false); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{1, 2, 3}); + + assertFalse("Expected invocation checks to fail over split screen", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + // Attempt is logged regardless. + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_isInSplitScreen_allowed() { + when(mMockStateManager.isInvocationAllowedInSplitscreen()).thenReturn(true); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{1, 2, 3}); + + assertTrue("Expected invocation checks to succeed over split screen", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + // Attempt is logged regardless. + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN); + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsAvailable_commitHapticEnabled() { + try (AutoCloseable flag = overrideSearchHapticCommitFlag(true)) { + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockContextualSearchHapticManager).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsAvailable_commitHapticDisabled() { + try (AutoCloseable flag = overrideSearchHapticCommitFlag(false)) { + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockContextualSearchHapticManager, never()).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsNotAvailable_commitHapticEnabled() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + try (AutoCloseable flag = overrideSearchHapticCommitFlag(true)) { + // Still expect true since this method doesn't run the checks. + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + // Still vibrate based on the flag. + verify(mMockContextualSearchHapticManager).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsNotAvailable_commitHapticDisabled() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + try (AutoCloseable flag = overrideSearchHapticCommitFlag(false)) { + // Still expect true since this method doesn't run the checks. + assertTrue("Expected ContextualSearch invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + // Still don't vibrate based on the flag. + verify(mMockContextualSearchHapticManager, never()).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private AutoCloseable overrideSearchHapticCommitFlag(boolean value) { + return TestExtensions.overrideNavConfigFlag( + "ENABLE_SEARCH_HAPTIC_COMMIT", + value, + () -> DeviceConfigWrapper.get().getEnableSearchHapticCommit()); + } +} diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java index fbd24d89f0..06d7a93993 100644 --- a/src/com/android/launcher3/logging/StatsLogManager.java +++ b/src/com/android/launcher3/logging/StatsLogManager.java @@ -798,6 +798,44 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User long pressed on the taskbar IME switcher button") LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS(1798), + @UiEvent(doc = "Failed to launch assistant due to Google assistant not available") + LAUNCHER_LAUNCH_ASSISTANT_FAILED_NOT_AVAILABLE(1465), + + @UiEvent(doc = "Failed to launch assistant due to service error") + LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR(1466), + + @UiEvent(doc = "User launched assistant by long-pressing nav handle") + LAUNCHER_LAUNCH_ASSISTANT_SUCCESSFUL_NAV_HANDLE(1467), + + @UiEvent(doc = "Failed to launch due to Contextual Search not available") + LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE(1471), + + @UiEvent(doc = "Failed to launch due to Contextual Search setting disabled") + LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED(1632), + + @UiEvent(doc = "User launched Contextual Search by long-pressing home in 3-button mode") + LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME(1481), + + @UiEvent(doc = "User launched Contextual Search by using accessibility System Action") + LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION(1492), + + @UiEvent(doc = "User launched Contextual Search by long pressing the meta key") + LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_META(1606), + + @UiEvent(doc = "Contextual Search invocation was attempted over the notification shade") + LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE(1485), + + @UiEvent(doc = "The Contextual Search all entrypoints toggle value in Settings") + LAUNCHER_SETTINGS_OMNI_ALL_ENTRYPOINTS_TOGGLE_VALUE(1633), + + @UiEvent(doc = "Contextual Search invocation was attempted over the keyguard") + LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD(1501), + + @UiEvent(doc = "Contextual Search invocation was attempted while splitscreen is active") + LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN(1505), + + @UiEvent(doc = "User long press nav handle and a long press runnable was created.") + LAUNCHER_OMNI_GET_LONG_PRESS_RUNNABLE(1545), // ADD MORE ; @@ -828,6 +866,10 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "The duration of asynchronous loading workspace") LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC(1367), + + @UiEvent(doc = "Time passed between Contextual Search runnable creation and execution. This" + + " ensures that Recent animations have finished before Contextual Search starts.") + LAUNCHER_LATENCY_OMNI_RUNNABLE(1546), ; private final int mId; From 518ca4609cd68fc5ba75946b54d3184e66b8a989 Mon Sep 17 00:00:00 2001 From: Toni Barzic Date: Fri, 11 Oct 2024 05:38:36 +0000 Subject: [PATCH 02/11] Optimize updating hotseat items in overflown taskbar The logic for updating hotseat items in taskbar involved adding all available recent items to the view, and then removing ones that overflow. It also preemptively added overflow icon, and then removed it when no overflow was detected. This cl updates logic to stop adding recent items before they cause overflow, and add overflow icon only when needed. Bug: 368119679 Test: Keep opening apps until taskbar enters overflow, and verify that expected number of app icons is displayed, and overflow icon is shown in the correct place. Flag: com.android.launcher3.taskbar_overflow Change-Id: Icaad4386b54bbfccba8ace5876d7fe5c90cb7798 --- .../launcher3/taskbar/TaskbarView.java | 63 +++++++------------ 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 8763509183..3ab88a1cc0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -412,13 +412,33 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) { addView(mTaskbarDividerContainer, nextViewIndex++); mAddedDividerForRecents = true; - if (mTaskbarOverflowView != null) { + } + + // At this point, the all apps button has not been added as a child view, but needs to be + // accounted for when comparing current icon count to max number of icons. + int nonTaskIconsToBeAdded = 1; + + boolean supportsOverflow = Flags.taskbarOverflow(); + if (supportsOverflow) { + int numberOfSupportedRecents = 0; + for (GroupTask task : recentTasks) { + // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. + if (!task.hasMultipleTasks()) { + ++numberOfSupportedRecents; + } + } + if (nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded > mMaxNumIcons + && mTaskbarOverflowView != null) { addView(mTaskbarOverflowView, nextViewIndex++); } } // Add Recent/Running icons. for (GroupTask task : recentTasks) { + if (supportsOverflow && nextViewIndex + nonTaskIconsToBeAdded >= mMaxNumIcons) { + break; + } + // Replace any Recent views with the appropriate type if it's not already that type. final int expectedLayoutResId; boolean isCollection = false; @@ -488,8 +508,6 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } } - updateRecentAppsToFit(); - if (mActivityContext.getDeviceProfile().isQsbInline) { addView(mQsb, mIsRtl ? getChildCount() : 0); // Always set QSB to invisible after re-adding. @@ -497,45 +515,6 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } } - /** - * Updates the recent apps portion of the taskbar by: - * - Removing overflow affordance if overflow is not needed. - * - Removing any recent apps that do not fit. - */ - private void updateRecentAppsToFit() { - if (!Flags.taskbarOverflow()) { - return; - } - int indexOfFirstRecentApp = -1; - int size = getChildCount(); - boolean removeOverflowView = true; - - for (int i = 0; i < size; ++i) { - if (getChildAt(i).getTag() instanceof GroupTask) { - indexOfFirstRecentApp = i; - removeOverflowView = false; - break; - } - } - - if (indexOfFirstRecentApp != -1) { - // We pre-maturely added the overflow icon, so we can take it out of the count. - int numRecentAppsToRemove = Math.max(0, getChildCount() - mMaxNumIcons + 1); - if (numRecentAppsToRemove <= 1) { - // We can fit all of the recent apps if we remove the overflow icon. - removeOverflowView = true; - } else { - for (int i = 0; i < numRecentAppsToRemove; ++i) { - removeAndRecycle(getChildAt(indexOfFirstRecentApp)); - } - } - } - - if (removeOverflowView) { - removeView(mTaskbarOverflowView); - } - } - /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */ public void applyGroupTaskToBubbleTextView(BubbleTextView btv, GroupTask groupTask) { // TODO(b/343289567): support app pairs. From dbd3c09623ab89b76782cae4eb05cf965217a500 Mon Sep 17 00:00:00 2001 From: Tracy Zhou Date: Sun, 13 Oct 2024 14:48:44 -0700 Subject: [PATCH 03/11] Disallow 2-finger swipe from the corners to trigger assistant from trackpad Bug: 358337581 Test: manual Flag: EXEMPT bugfix Change-Id: I62994afe0e3edaaa990f58b44b87efa16f0580ec --- .../src/com/android/quickstep/RecentsAnimationDeviceState.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java index 51317743cd..99d48aa22a 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java @@ -19,6 +19,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.launcher3.MotionEventsUtils.isTrackpadScroll; import static com.android.launcher3.util.DisplayController.CHANGE_ALL; import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE; import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION; @@ -563,6 +564,7 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, E return mAssistantAvailable && !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags) && mRotationTouchHelper.touchInAssistantRegion(ev) + && !isTrackpadScroll(ev) && !isLockToAppActive(); } From 2f4ccc63b62215317fc79cf9ac02fee49e82336d Mon Sep 17 00:00:00 2001 From: Graciela Wissen Putri Date: Mon, 14 Oct 2024 10:32:22 +0000 Subject: [PATCH 04/11] Add task menu item to move task to external display Call SystemUiProxy.moveToExternalDisplay to move existing Overview task to desktop in external display Bug: 372872848 Test: atest NexusLauncherTests:com.android.quickstep.ExternalDisplaySystemShortcutTest Flag: com.android.window.flags.move_to_external_display_shortcut Change-Id: I096a9839956ab5cab86bd0aaabc625a8587ca42a --- .../res/drawable/ic_external_display.xml | 24 +++ quickstep/res/values/strings.xml | 2 + .../DesktopRecentsTransitionController.kt | 5 + .../ExternalDisplaySystemShortcut.kt | 86 ++++++++ .../com/android/quickstep/SystemUiProxy.java | 11 + .../android/quickstep/TaskOverlayFactory.java | 1 + .../android/quickstep/views/RecentsView.java | 20 ++ .../ExternalDisplaySystemShortcutTest.kt | 193 ++++++++++++++++++ .../launcher3/logging/StatsLogManager.java | 3 + 9 files changed, 345 insertions(+) create mode 100644 quickstep/res/drawable/ic_external_display.xml create mode 100644 quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt create mode 100644 quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt diff --git a/quickstep/res/drawable/ic_external_display.xml b/quickstep/res/drawable/ic_external_display.xml new file mode 100644 index 0000000000..64c183ea12 --- /dev/null +++ b/quickstep/res/drawable/ic_external_display.xml @@ -0,0 +1,24 @@ + + + + diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index 008766b51b..91c2a6d776 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -28,6 +28,8 @@ Freeform Desktop + + Move to external display Desktop diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt index 8b3a032137..ac1ffa6029 100644 --- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt +++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt @@ -66,6 +66,11 @@ class DesktopRecentsTransitionController( systemUiProxy.moveToDesktop(taskId, transitionSource) } + /** Move task to external display from recents view */ + fun moveToExternalDisplay(taskId: Int) { + systemUiProxy.moveToExternalDisplay(taskId) + } + private class RemoteDesktopLaunchTransitionRunner( private val desktopTaskView: DesktopTaskView, private val animated: Boolean, diff --git a/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt b/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt new file mode 100644 index 0000000000..46c4f365be --- /dev/null +++ b/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt @@ -0,0 +1,86 @@ +/* + * 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.view.View +import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.R +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.popup.SystemShortcut +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskContainer +import com.android.window.flags.Flags +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus + +/** A menu item that allows the user to move the current app into external display. */ +class ExternalDisplaySystemShortcut( + container: RecentsViewContainer, + abstractFloatingViewHelper: AbstractFloatingViewHelper, + private val taskContainer: TaskContainer, +) : + SystemShortcut( + R.drawable.ic_external_display, + R.string.recent_task_option_external_display, + container, + taskContainer.itemInfo, + taskContainer.taskView, + abstractFloatingViewHelper, + ) { + override fun onClick(view: View) { + dismissTaskMenuView() + val recentsView = mTarget.getOverviewPanel>() + recentsView.moveTaskToExternalDisplay(taskContainer) { + mTarget.statsLogManager + .logger() + .withItemInfo(taskContainer.itemInfo) + .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP) + } + } + + companion object { + @JvmOverloads + /** + * Creates a factory for creating move task to external display system shortcuts in + * [com.android.quickstep.TaskOverlayFactory]. + */ + fun createFactory( + abstractFloatingViewHelper: AbstractFloatingViewHelper = AbstractFloatingViewHelper() + ): TaskShortcutFactory = + object : TaskShortcutFactory { + override fun getShortcuts( + container: RecentsViewContainer, + taskContainer: TaskContainer, + ): List? { + return if ( + DesktopModeStatus.canEnterDesktopMode(container.asContext()) && + Flags.moveToExternalDisplayShortcut() + ) + listOf( + ExternalDisplaySystemShortcut( + container, + abstractFloatingViewHelper, + taskContainer, + ) + ) + else null + } + + override fun showForGroupedTask() = true + } + } +} diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index 7a0d701724..967bd184c3 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -1493,6 +1493,17 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable { } } + /** Call shell to move a task with given `taskId` to external display. */ + public void moveToExternalDisplay(int taskId) { + if (mDesktopMode != null) { + try { + mDesktopMode.moveToExternalDisplay(taskId); + } catch (RemoteException e) { + Log.w(TAG, "Failed call moveToExternalDisplay", e); + } + } + } + // // Unfold transition // diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java index 8e45767f85..0dbdcb713a 100644 --- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java +++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java @@ -116,6 +116,7 @@ public class TaskOverlayFactory implements ResourceBasedOverride { TaskShortcutFactory.INSTALL, TaskShortcutFactory.FREE_FORM, DesktopSystemShortcut.Companion.createFactory(), + ExternalDisplaySystemShortcut.Companion.createFactory(), TaskShortcutFactory.WELLBEING, TaskShortcutFactory.SAVE_APP_PAIR, TaskShortcutFactory.SCREENSHOT, diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 7554c4496a..bccfa0c756 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -6673,6 +6673,26 @@ public abstract class RecentsView< successCallback.run(); } + /** + * Move the provided task into external display and invoke {@code successCallback} if succeeded. + */ + public void moveTaskToExternalDisplay(TaskContainer taskContainer, Runnable successCallback) { + if (!DesktopModeStatus.canEnterDesktopMode(mContext)) { + return; + } + switchToScreenshot(() -> finishRecentsAnimation(/* toRecents= */true, /* shouldPip= */false, + () -> moveTaskToDesktopInternal(taskContainer, successCallback))); + } + + private void moveTaskToDesktopInternal(TaskContainer taskContainer, Runnable successCallback) { + if (mDesktopRecentsTransitionController == null) { + return; + } + mDesktopRecentsTransitionController.moveToExternalDisplay(taskContainer.getTask().key.id); + successCallback.run(); + } + + // Logs when the orientation of Overview changes. We log both real and fake orientation changes. private void logOrientationChanged() { // Only log when Overview is showing. diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt new file mode 100644 index 0000000000..8968b9c55a --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt @@ -0,0 +1,193 @@ +/* + * 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.content.ComponentName +import android.content.Intent +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.uioverrides.QuickstepLauncher +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.quickstep.TaskOverlayFactory.TaskOverlay +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.views.LauncherRecentsView +import com.android.quickstep.views.TaskContainer +import com.android.quickstep.views.TaskThumbnailViewDeprecated +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskViewIcon +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.android.window.flags.Flags +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +/** Test for ExternalDisplaySystemShortcut */ +class ExternalDisplaySystemShortcutTest { + + @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) + + private val launcher: QuickstepLauncher = mock() + private val statsLogManager: StatsLogManager = mock() + private val statsLogger: StatsLogManager.StatsLogger = mock() + private val recentsView: LauncherRecentsView = mock() + private val taskView: TaskView = mock() + private val workspaceItemInfo: WorkspaceItemInfo = mock() + private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock() + private val iconView: TaskViewIcon = mock() + private val transformingTouchDelegate: TransformingTouchDelegate = mock() + private val factory: TaskShortcutFactory = + ExternalDisplaySystemShortcut.createFactory(abstractFloatingViewHelper) + private val overlayFactory: TaskOverlayFactory = mock() + private val overlay: TaskOverlay<*> = mock() + + private lateinit var mockitoSession: StaticMockitoSession + + @Before + fun setUp() { + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.enforceDeviceRestrictions() } + ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + whenever(overlayFactory.createOverlay(any())).thenReturn(overlay) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @EnableFlags(Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT) + fun createExternalDisplayTaskShortcut_desktopModeDisabled() { + val task = createTask() + val taskContainer = createTaskContainer(task) + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + ) + fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported() { + ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + + val taskContainer = createTaskContainer(createTask()) + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + ) + fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported_overrideEnabled() { + ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.enforceDeviceRestrictions() } + + val taskContainer = spy(createTaskContainer(createTask())) + doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNotNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + ) + fun externalDisplaySystemShortcutClicked() { + val task = createTask() + val taskContainer = spy(createTaskContainer(task)) + + whenever(launcher.getOverviewPanel()).thenReturn(recentsView) + whenever(launcher.statsLogManager).thenReturn(statsLogManager) + whenever(statsLogManager.logger()).thenReturn(statsLogger) + whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger) + whenever(recentsView.moveTaskToExternalDisplay(any(), any())).thenAnswer { + val successCallback = it.getArgument(1) + successCallback.run() + } + doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).hasSize(1) + assertThat(shortcuts!!.first()).isInstanceOf(ExternalDisplaySystemShortcut::class.java) + + val externalDisplayShortcut = shortcuts.first() as ExternalDisplaySystemShortcut + + externalDisplayShortcut.onClick(taskView) + + val allTypesExceptRebindSafe = + AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv() + verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe) + verify(recentsView).moveTaskToExternalDisplay(eq(taskContainer), any()) + verify(statsLogger).withItemInfo(workspaceItemInfo) + verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP) + } + + private fun createTask(): Task = Task(TaskKey(1, 0, Intent(), ComponentName("", ""), 0, 2000)) + + private fun createTaskContainer(task: Task): TaskContainer { + val snapshotView = + if (enableRefactorTaskThumbnail()) mock() + else mock() + return TaskContainer( + taskView, + task, + snapshotView, + iconView, + transformingTouchDelegate, + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + digitalWellBeingToast = null, + showWindowsView = null, + overlayFactory, + ) + } +} diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java index fbd24d89f0..a48405ca50 100644 --- a/src/com/android/launcher3/logging/StatsLogManager.java +++ b/src/com/android/launcher3/logging/StatsLogManager.java @@ -221,6 +221,9 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User tapped on desktop icon on a task menu.") LAUNCHER_SYSTEM_SHORTCUT_DESKTOP_TAP(1706), + @UiEvent(doc = "Use tapped on external display icon on a task menu,") + LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP(1957), + @UiEvent(doc = "User tapped on pause app system shortcut.") LAUNCHER_SYSTEM_SHORTCUT_PAUSE_TAP(521), From 7eae20bcb11ab487ae2b16083ba8b4ba37463c85 Mon Sep 17 00:00:00 2001 From: Jordan Silva Date: Thu, 10 Oct 2024 11:17:58 +0100 Subject: [PATCH 05/11] Update OverviewCommandHelper to use Executors.MAIN to reduce the percentage of missed frames This change will reduce the increase in the amount of missed frames measured by crystalball. I believe coroutines launch is competing with Executors.MAIN for scheduling and running tasks in the main thread, thus the increase in missing frames. Fix: 366077002 Flag: com.android.launcher3.enable_overview_command_helper_timeout Test: OverviewCommandHelperTest Test: android.platform.test.scenario.launcher.CloseApp3ButtonModeMicrobenchmark#testOpenLauncher Test: atp:v2/android-crystalball-eng/health/microbench/launcher/main/launcher-action-suite Change-Id: I4477879d4f065ec3883f2c3cb3ef044e973ce0cb --- .../quickstep/OverviewCommandHelper.kt | 10 +++----- .../quickstep/OverviewCommandHelperTest.kt | 25 +++++++++++++++---- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt index 461f963dc1..e23947b62d 100644 --- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt @@ -53,6 +53,7 @@ import com.android.systemui.shared.recents.model.ThumbnailData import com.android.systemui.shared.system.InteractionJankMonitorWrapper import java.io.PrintWriter import java.util.concurrent.ConcurrentLinkedDeque +import java.util.concurrent.Executor import kotlin.coroutines.resume import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -69,6 +70,7 @@ constructor( private val overviewComponentObserver: OverviewComponentObserver, private val taskAnimationManager: TaskAnimationManager, private val dispatcherProvider: DispatcherProvider = ProductionDispatchers, + private val uiExecutor: Executor = Executors.MAIN_EXECUTOR, ) { private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.default) @@ -85,7 +87,7 @@ constructor( get() = overviewComponentObserver.containerInterface private val visibleRecentsView: RecentsView<*, *>? - get() = containerInterface.getVisibleRecentsView>() + get() = containerInterface.getVisibleRecentsView() /** * Adds a command to be executed next, after all pending tasks are completed. Max commands that @@ -105,11 +107,7 @@ constructor( if (commandQueue.size == 1) { Log.d(TAG, "execute: $command - queue size: ${commandQueue.size}") - if (enableOverviewCommandHelperTimeout()) { - coroutineScope.launch(dispatcherProvider.main) { processNextCommand() } - } else { - Executors.MAIN_EXECUTOR.execute { processNextCommand() } - } + uiExecutor.execute { processNextCommand() } } else { Log.d(TAG, "not executed: $command - queue size: ${commandQueue.size}") } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt index 0ae710f866..b0db737c94 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt @@ -41,7 +41,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.doAnswer import org.mockito.Mockito.spy -import org.mockito.Mockito.`when` import org.mockito.kotlin.any import org.mockito.kotlin.mock @@ -56,10 +55,12 @@ class OverviewCommandHelperTest { private val testScope = TestScope(dispatcher) private var pendingCallbacksWithDelays = mutableListOf() + private lateinit var pendingCommandsToExecute: MutableList @Suppress("UNCHECKED_CAST") @Before fun setup() { + pendingCommandsToExecute = mutableListOf() setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT) sut = @@ -68,7 +69,8 @@ class OverviewCommandHelperTest { touchInteractionService = mock(), overviewComponentObserver = mock(), taskAnimationManager = mock(), - dispatcherProvider = TestDispatcherProvider(dispatcher) + dispatcherProvider = TestDispatcherProvider(dispatcher), + uiExecutor = { runnable -> pendingCommandsToExecute += runnable }, ) ) @@ -94,12 +96,21 @@ class OverviewCommandHelperTest { pendingCallbacksWithDelays.add(delayInMillis) } + /** + * This function runs all the pending commands from the Executor for testing purposes. Replacing + * the uiExecutor allows the test to execute the command queue manually, making it possible to + * assert each state of the commands in the queue individually. + */ + private fun executePendingCommands() = pendingCommandsToExecute.forEach { it.run() } + @Test fun whenFirstCommandIsAdded_executeCommandImmediately() = testScope.runTest { // Add command to queue val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME)!! assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + executePendingCommands() + assertThat(commandInfo.status).isEqualTo(CommandStatus.PROCESSING) runCurrent() assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) } @@ -114,7 +125,7 @@ class OverviewCommandHelperTest { val commandInfo: CommandInfo = sut.addCommand(commandType)!! assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) - runCurrent() + executePendingCommands() assertThat(commandInfo.status).isEqualTo(CommandStatus.PROCESSING) advanceTimeBy(200L) @@ -135,12 +146,14 @@ class OverviewCommandHelperTest { val commandInfo2: CommandInfo = sut.addCommand(commandType2)!! assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) - runCurrent() + executePendingCommands() assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING) assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) advanceTimeBy(101L) assertThat(commandInfo1.status).isEqualTo(CommandStatus.COMPLETED) + + executePendingCommands() assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING) advanceTimeBy(101L) @@ -161,12 +174,14 @@ class OverviewCommandHelperTest { val commandInfo2: CommandInfo = sut.addCommand(commandType2)!! assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) - runCurrent() + executePendingCommands() assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING) assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) advanceTimeBy(QUEUE_TIMEOUT) assertThat(commandInfo1.status).isEqualTo(CommandStatus.CANCELED) + + executePendingCommands() assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING) advanceTimeBy(101) From a3ec98e06d818c20a30b3be9b86b5c0ba2310e1b Mon Sep 17 00:00:00 2001 From: Brian Isganitis Date: Mon, 14 Oct 2024 12:48:50 -0400 Subject: [PATCH 06/11] Migrate to FakeLauncherPrefs for Taskbar unit tests. Deprecates the preference rules. Flag: TEST_ONLY Test: go/testedequals Bug: 230027385 Change-Id: Ic3a9e589b556232d196942d687e2e2bddc000877 --- .../taskbar/TaskbarControllerTestUtil.kt | 24 ++++ .../TaskbarEduTooltipControllerTest.kt | 42 +++---- .../taskbar/TaskbarStashControllerTest.kt | 19 +-- .../TaskbarAllAppsViewControllerTest.kt | 12 +- .../rules/TaskbarPinningPreferenceRule.kt | 65 ---------- .../rules/TaskbarPinningPreferenceRuleTest.kt | 114 ------------------ .../taskbar/rules/TaskbarPreferenceRule.kt | 55 --------- .../rules/TaskbarPreferenceRuleTest.kt | 67 ---------- .../rules/TaskbarWindowSandboxContext.kt | 6 + .../android/launcher3/FakeLauncherPrefs.kt | 6 +- .../launcher3/FakeLauncherPrefsTest.kt | 7 +- 11 files changed, 72 insertions(+), 345 deletions(-) delete mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt delete mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt delete mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt delete mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt index a57fb702d6..6e2f74acc7 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt @@ -16,10 +16,34 @@ package com.android.launcher3.taskbar +import android.content.Context import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.ConstantItem +import com.android.launcher3.LauncherPrefs +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty object TaskbarControllerTestUtil { inline fun runOnMainSync(crossinline runTest: () -> Unit) { getInstrumentation().runOnMainSync { runTest() } } + + /** Returns a property to read/write the value of a [ConstantItem]. */ + fun ConstantItem.asProperty(context: Context): ReadWriteProperty { + return TaskbarItemProperty(context, this) + } + + private class TaskbarItemProperty( + private val context: Context, + private val item: ConstantItem, + ) : ReadWriteProperty { + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return LauncherPrefs.get(context).get(item) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + runOnMainSync { LauncherPrefs.get(context).put(item, value) } + } + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt index af05d22391..e60717b6d8 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt @@ -18,14 +18,13 @@ package com.android.launcher3.taskbar import android.util.Log import com.android.launcher3.Utilities +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync import com.android.launcher3.taskbar.rules.TaskbarModeRule import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode -import com.android.launcher3.taskbar.rules.TaskbarPinningPreferenceRule -import com.android.launcher3.taskbar.rules.TaskbarPreferenceRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext @@ -47,19 +46,9 @@ class TaskbarEduTooltipControllerTest { @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() - @get:Rule(order = 1) - val tooltipStepPreferenceRule = - TaskbarPreferenceRule(context, OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.prefItem) + @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context) - @get:Rule(order = 2) - val searchEduPreferenceRule = - TaskbarPreferenceRule(context, OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN) - - @get:Rule(order = 3) val taskbarPinningPreferenceRule = TaskbarPinningPreferenceRule(context) - - @get:Rule(order = 4) val taskbarModeRule = TaskbarModeRule(context) - - @get:Rule(order = 5) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) @InjectController lateinit var taskbarEduTooltipController: TaskbarEduTooltipController @@ -68,6 +57,9 @@ class TaskbarEduTooltipControllerTest { private val wasInTestHarness = Utilities.isRunningInTestHarness() + private var tooltipStep by OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.prefItem.asProperty(context) + private var searchEduSeen by OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN.asProperty(context) + @Before fun setUp() { Log.e("Taskbar", "TaskbarEduTooltipControllerTest test started") @@ -85,7 +77,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(THREE_BUTTONS) fun testMaybeShowSwipeEdu_whenTaskbarIsInThreeButtonMode_doesNotShowSwipeEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE + tooltipStep = TOOLTIP_STEP_SWIPE assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) @@ -95,7 +87,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(TRANSIENT) fun testMaybeShowSwipeEdu_whenSwipeEduAlreadyShown_doesNotShowSwipeEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_FEATURES + tooltipStep = TOOLTIP_STEP_FEATURES assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) @@ -105,7 +97,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(TRANSIENT) fun testMaybeShowSwipeEdu_whenUserHasNotSeen_doesShowSwipeEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE + tooltipStep = TOOLTIP_STEP_SWIPE assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) @@ -115,7 +107,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(TRANSIENT) fun testMaybeShowFeaturesEdu_whenFeatureEduAlreadyShown_doesNotShowFeatureEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_NONE + tooltipStep = TOOLTIP_STEP_NONE assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) @@ -125,7 +117,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(TRANSIENT) fun testMaybeShowFeaturesEdu_whenUserHasNotSeen_doesShowFeatureEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_FEATURES + tooltipStep = TOOLTIP_STEP_FEATURES assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) @@ -135,7 +127,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(THREE_BUTTONS) fun testMaybeShowPinningEdu_whenTaskbarIsInThreeButtonMode_doesNotShowPinningEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_PINNING + tooltipStep = TOOLTIP_STEP_PINNING assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) @@ -146,7 +138,7 @@ class TaskbarEduTooltipControllerTest { @TaskbarMode(TRANSIENT) fun testMaybeShowPinningEdu_whenUserHasNotSeen_doesShowPinningEdu() { // Test standalone pinning edu, where user has seen taskbar edu before, but not pinning edu. - tooltipStepPreferenceRule.value = TOOLTIP_STEP_PINNING + tooltipStep = TOOLTIP_STEP_PINNING assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) @@ -156,21 +148,21 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(TRANSIENT) fun testIsBeforeTooltipFeaturesStep_whenUserHasNotSeenFeatureEdu_shouldReturnTrue() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE + tooltipStep = TOOLTIP_STEP_SWIPE assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isTrue() } @Test @TaskbarMode(TRANSIENT) fun testIsBeforeTooltipFeaturesStep_whenUserHasSeenFeatureEdu_shouldReturnFalse() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_NONE + tooltipStep = TOOLTIP_STEP_NONE assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isFalse() } @Test @TaskbarMode(TRANSIENT) fun testHide_whenTooltipIsOpen_shouldCloseTooltip() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE + tooltipStep = TOOLTIP_STEP_SWIPE assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } @@ -190,7 +182,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(PINNED) fun testMaybeShowSearchEdu_whenTaskbarIsPinnedAndUserHasSeenSearchEdu_shouldNotShowSearchEdu() { - searchEduPreferenceRule.value = true + searchEduSeen = true assertThat(taskbarEduTooltipController.userHasSeenSearchEdu).isTrue() runOnMainSync { taskbarEduTooltipController.hide() } assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt index de73ce7016..71f4ef4ee6 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt @@ -18,10 +18,13 @@ package com.android.launcher3.taskbar import android.animation.AnimatorTestRule import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING import com.android.launcher3.R import com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_STASHED import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE @@ -42,7 +45,6 @@ import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode -import com.android.launcher3.taskbar.rules.TaskbarPinningPreferenceRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode @@ -63,11 +65,11 @@ import org.junit.runner.RunWith @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @EmulatedDevices(["pixelTablet2023"]) class TaskbarStashControllerTest { - @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() - @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context) - @get:Rule(order = 2) val taskbarPinningPreferenceRule = TaskbarPinningPreferenceRule(context) - @get:Rule(order = 3) val animatorTestRule = AnimatorTestRule(this) - @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context) + @get:Rule(order = 4) val animatorTestRule = AnimatorTestRule(this) + @get:Rule(order = 5) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) @InjectController lateinit var stashController: TaskbarStashController @InjectController lateinit var viewController: TaskbarViewController @@ -121,10 +123,11 @@ class TaskbarStashControllerTest { @Test fun testRecreateAsTransient_timeoutStarted() { - taskbarPinningPreferenceRule.isPinned = true + var isPinned by TASKBAR_PINNING.asProperty(context) + isPinned = true activityContext.controllers.sharedState?.taskbarWasPinned = true - taskbarPinningPreferenceRule.isPinned = false + isPinned = false assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt index 516220a803..3c0d9c6a0e 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt @@ -21,6 +21,7 @@ import com.android.launcher3.R import com.android.launcher3.appprediction.AppsDividerView import com.android.launcher3.appprediction.AppsDividerView.DividerType import com.android.launcher3.appprediction.PredictionRowView +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty import com.android.launcher3.taskbar.TaskbarStashController import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IN_APP_AUTO import com.android.launcher3.taskbar.allapps.TaskbarAllAppsControllerTest.Companion.TEST_PREDICTED_APPS @@ -29,7 +30,6 @@ import com.android.launcher3.taskbar.rules.TaskbarModeRule import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode -import com.android.launcher3.taskbar.rules.TaskbarPreferenceRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext @@ -49,14 +49,12 @@ class TaskbarAllAppsViewControllerTest { @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context) - @get:Rule(order = 2) - val allAppsVisitedPreferenceRule = - TaskbarPreferenceRule(context, ALL_APPS_VISITED_COUNT.prefItem) - @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) @InjectController lateinit var overlayController: TaskbarOverlayController @InjectController lateinit var stashController: TaskbarStashController + private var allAppsVisitedCount by ALL_APPS_VISITED_COUNT.prefItem.asProperty(context) private val searchSessionController = TestUtil.getOnUiThread { TaskbarSearchSessionController.newInstance(context) } @@ -102,7 +100,7 @@ class TaskbarAllAppsViewControllerTest { @Test fun testShow_firstAllAppsVisit_hasAllAppsTextDivider() { - allAppsVisitedPreferenceRule.value = 0 + allAppsVisitedCount = 0 val viewController = createViewController() getInstrumentation().runOnMainSync { viewController.show(false) } @@ -120,7 +118,7 @@ class TaskbarAllAppsViewControllerTest { @Test fun testShow_maxAllAppsVisitedCount_hasLineDivider() { - allAppsVisitedPreferenceRule.value = ALL_APPS_VISITED_COUNT.maxCount + allAppsVisitedCount = ALL_APPS_VISITED_COUNT.maxCount val viewController = createViewController() getInstrumentation().runOnMainSync { viewController.show(false) } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt deleted file mode 100644 index d417790d84..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.launcher3.taskbar.rules - -import android.platform.test.flag.junit.FlagsParameterization -import android.platform.test.flag.junit.SetFlagsRule -import com.android.launcher3.Flags.FLAG_ENABLE_TASKBAR_PINNING -import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING -import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING_IN_DESKTOP_MODE -import com.android.launcher3.util.DisplayController -import org.junit.rules.RuleChain -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement - -/** - * Rule that allows modifying the Taskbar pinned preferences. - * - * The original preference values are restored on teardown. - * - * If this rule is being used with [TaskbarUnitTestRule], make sure this rule is applied first. - * - * This rule is overkill if a test does not need to change the mode during Taskbar's lifecycle. If - * the mode is static, use [TaskbarModeRule] instead, which forces the mode. A test can class can - * declare both this rule and [TaskbarModeRule] but using both for a test method is unsupported. - */ -class TaskbarPinningPreferenceRule(context: TaskbarWindowSandboxContext) : TestRule { - - private val setFlagsRule = - SetFlagsRule(FlagsParameterization(mapOf(FLAG_ENABLE_TASKBAR_PINNING to true))) - private val pinningRule = TaskbarPreferenceRule(context, TASKBAR_PINNING) - private val desktopPinningRule = TaskbarPreferenceRule(context, TASKBAR_PINNING_IN_DESKTOP_MODE) - private val ruleChain = - RuleChain.outerRule(setFlagsRule).around(pinningRule).around(desktopPinningRule) - - var isPinned by pinningRule::value - var isPinnedInDesktopMode by desktopPinningRule::value - - override fun apply(base: Statement, description: Description): Statement { - return object : Statement() { - override fun evaluate() { - DisplayController.enableTaskbarModePreferenceForTests(true) - try { - ruleChain.apply(base, description).evaluate() - } finally { - DisplayController.enableTaskbarModePreferenceForTests(false) - } - } - } - } -} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt deleted file mode 100644 index 977e7a5fa3..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.launcher3.taskbar.rules - -import com.android.launcher3.util.DisplayController -import com.android.launcher3.util.LauncherMultivalentJUnit -import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices -import com.android.launcher3.util.window.WindowManagerProxy -import com.google.android.apps.nexuslauncher.deviceemulator.TestWindowManagerProxy -import com.google.common.truth.Truth.assertThat -import org.junit.Rule -import org.junit.Test -import org.junit.runner.Description -import org.junit.runner.RunWith -import org.junit.runners.model.Statement - -@RunWith(LauncherMultivalentJUnit::class) -@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) -class TaskbarPinningPreferenceRuleTest { - @get:Rule val context = TaskbarWindowSandboxContext.create() - - private val preferenceRule = TaskbarPinningPreferenceRule(context) - - @Test - fun testEnablePinning_verifyDisplayController() { - onSetup { - preferenceRule.isPinned = true - preferenceRule.isPinnedInDesktopMode = false - assertThat(DisplayController.isPinnedTaskbar(context)).isTrue() - } - } - - @Test - fun testDisablePinning_verifyDisplayController() { - onSetup { - preferenceRule.isPinned = false - preferenceRule.isPinnedInDesktopMode = false - assertThat(DisplayController.isPinnedTaskbar(context)).isFalse() - } - } - - @Test - fun testEnableDesktopPinning_verifyDisplayController() { - context.putObject( - WindowManagerProxy.INSTANCE, - TestWindowManagerProxy(context).apply { isInDesktopMode = true }, - ) - - onSetup { - preferenceRule.isPinned = false - preferenceRule.isPinnedInDesktopMode = true - assertThat(DisplayController.isPinnedTaskbar(context)).isTrue() - } - } - - @Test - fun testDisableDesktopPinning_verifyDisplayController() { - context.putObject( - WindowManagerProxy.INSTANCE, - TestWindowManagerProxy(context).apply { isInDesktopMode = true }, - ) - - onSetup { - preferenceRule.isPinned = false - preferenceRule.isPinnedInDesktopMode = false - assertThat(DisplayController.isPinnedTaskbar(context)).isFalse() - } - } - - @Test - fun testTearDown_afterTogglingPinnedPreference_preferenceReset() { - val wasPinned = preferenceRule.isPinned - onSetup { preferenceRule.isPinned = !preferenceRule.isPinned } - assertThat(preferenceRule.isPinned).isEqualTo(wasPinned) - } - - @Test - fun testTearDown_afterTogglingDesktopPreference_preferenceReset() { - val wasPinnedInDesktopMode = preferenceRule.isPinnedInDesktopMode - onSetup { preferenceRule.isPinnedInDesktopMode = !preferenceRule.isPinnedInDesktopMode } - assertThat(preferenceRule.isPinnedInDesktopMode).isEqualTo(wasPinnedInDesktopMode) - } - - /** Executes [runTest] after the [preferenceRule] setup phase completes. */ - private fun onSetup(runTest: () -> Unit) { - preferenceRule - .apply( - object : Statement() { - override fun evaluate() = runTest() - }, - DESCRIPTION, - ) - .evaluate() - } - - private companion object { - private val DESCRIPTION = - Description.createSuiteDescription(TaskbarPinningPreferenceRule::class.java) - } -} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt deleted file mode 100644 index e42ca9ea30..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.launcher3.taskbar.rules - -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import com.android.launcher3.ConstantItem -import com.android.launcher3.LauncherPrefs -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement - -/** - * Rule for modifying a Taskbar preference. - * - * The original preference value is restored on teardown. - */ -class TaskbarPreferenceRule( - private val context: TaskbarWindowSandboxContext, - private val constantItem: ConstantItem, -) : TestRule { - - private val prefs: LauncherPrefs - get() = LauncherPrefs.get(context) - - var value: T - get() = prefs.get(constantItem) - set(value) = getInstrumentation().runOnMainSync { prefs.put(constantItem, value) } - - override fun apply(base: Statement, description: Description): Statement { - return object : Statement() { - override fun evaluate() { - val originalValue = value - try { - base.evaluate() - } finally { - value = originalValue - } - } - } - } -} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt deleted file mode 100644 index b7e6fa3f35..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.launcher3.taskbar.rules - -import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING -import com.android.launcher3.util.LauncherMultivalentJUnit -import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices -import com.google.common.truth.Truth.assertThat -import org.junit.Rule -import org.junit.Test -import org.junit.runner.Description -import org.junit.runner.RunWith -import org.junit.runners.model.Statement - -@RunWith(LauncherMultivalentJUnit::class) -@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) -class TaskbarPreferenceRuleTest { - - @get:Rule val context = TaskbarWindowSandboxContext.create() - private val preferenceRule = TaskbarPreferenceRule(context, TASKBAR_PINNING) - - @Test - fun testSetup_toggleBoolean_updatesPreferences() { - val originalValue = preferenceRule.value - onSetup { - preferenceRule.value = !preferenceRule.value - assertThat(preferenceRule.value).isNotEqualTo(originalValue) - } - } - - @Test - fun testTeardown_afterTogglingBoolean_preferenceReset() { - val originalValue = preferenceRule.value - onSetup { preferenceRule.value = !preferenceRule.value } - assertThat(preferenceRule.value).isEqualTo(originalValue) - } - - private fun onSetup(runTest: () -> Unit) { - preferenceRule - .apply( - object : Statement() { - override fun evaluate() = runTest() - }, - DESCRIPTION, - ) - .evaluate() - } - - private companion object { - private val DESCRIPTION = - Description.createSuiteDescription(TaskbarPreferenceRule::class.java) - } -} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt index 7728504e11..2d3bfd6556 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt @@ -22,6 +22,8 @@ import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.view.Display.DEFAULT_DISPLAY import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.FakeLauncherPrefs +import com.android.launcher3.LauncherPrefs import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox import com.android.launcher3.util.SandboxApplication import org.junit.rules.ExternalResource @@ -40,6 +42,10 @@ private constructor(base: SandboxApplication, val virtualDisplay: VirtualDisplay ObjectSandbox by base, TestRule by RuleChain.outerRule(virtualDisplayRule(virtualDisplay)).around(base) { + init { + putObject(LauncherPrefs.INSTANCE, FakeLauncherPrefs(this)) + } + companion object { private const val VIRTUAL_DISPLAY_NAME = "TaskbarSandboxDisplay" diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt index 058ac05fef..946bbc55ca 100644 --- a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt +++ b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt @@ -17,6 +17,7 @@ package com.android.launcher3 import android.content.Context +import com.android.launcher3.util.Executors.MAIN_EXECUTOR /** Emulates Launcher preferences for a test environment. */ class FakeLauncherPrefs(private val context: Context) : LauncherPrefs() { @@ -69,5 +70,8 @@ class FakeLauncherPrefs(private val context: Context) : LauncherPrefs() { override fun close() = Unit - private fun notifyChange(key: String) = listeners.forEach { it.onPrefChanged(key) } + private fun notifyChange(key: String) { + // Mimics SharedPreferencesImpl#notifyListeners main thread dispatching. + MAIN_EXECUTOR.execute { listeners.forEach { it.onPrefChanged(key) } } + } } diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt index b8e347c14b..2463c93d83 100644 --- a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt @@ -17,6 +17,7 @@ package com.android.launcher3 import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.launcher3.util.LauncherMultivalentJUnit import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -107,7 +108,7 @@ class FakeLauncherPrefsTest { fun testAddListener_changeItemInPrefs_callsListener() { var changedKey: String? = null launcherPrefs.addListener({ changedKey = it }, TEST_CONSTANT_ITEM) - launcherPrefs.put(TEST_CONSTANT_ITEM, true) + getInstrumentation().runOnMainSync { launcherPrefs.put(TEST_CONSTANT_ITEM, true) } assertThat(changedKey).isEqualTo(TEST_CONSTANT_ITEM.sharedPrefKey) } @@ -117,7 +118,7 @@ class FakeLauncherPrefsTest { launcherPrefs.put(TEST_CONSTANT_ITEM, true) launcherPrefs.addListener({ changedKey = it }, TEST_CONSTANT_ITEM) - launcherPrefs.remove(TEST_CONSTANT_ITEM) + getInstrumentation().runOnMainSync { launcherPrefs.remove(TEST_CONSTANT_ITEM) } assertThat(changedKey).isEqualTo(TEST_CONSTANT_ITEM.sharedPrefKey) } @@ -128,7 +129,7 @@ class FakeLauncherPrefsTest { launcherPrefs.addListener(listener, TEST_CONSTANT_ITEM) launcherPrefs.removeListener(listener) - launcherPrefs.put(TEST_CONSTANT_ITEM, true) + getInstrumentation().runOnMainSync { launcherPrefs.put(TEST_CONSTANT_ITEM, true) } assertThat(changedKey).isNull() } } From 58fc65dedf22a30e5b8a61736b90a278c975eebd Mon Sep 17 00:00:00 2001 From: Sihua Ma Date: Wed, 9 Oct 2024 22:58:55 +0000 Subject: [PATCH 07/11] [Dagger] Make CustomWidgetManager provided by DaggerSingletonObject Test: presubmit Bug: 361850561 Flag: NONE Dagger change Change-Id: I9c2a9cc12aa2a409e8f9053a059426843f72deed --- .../dagger/LauncherBaseAppComponent.java | 2 + .../widget/custom/CustomWidgetManager.java | 60 ++++++++++++------- .../widget/custom/CustomWidgetManagerTest.kt | 4 +- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java index 088277b5ca..c3508b77db 100644 --- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java +++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java @@ -22,6 +22,7 @@ import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.util.DaggerSingletonTracker; import com.android.launcher3.util.ScreenOnTracker; import com.android.launcher3.util.SettingsCache; +import com.android.launcher3.widget.custom.CustomWidgetManager; import dagger.BindsInstance; @@ -38,6 +39,7 @@ public interface LauncherBaseAppComponent { InstallSessionHelper getInstallSessionHelper(); ScreenOnTracker getScreenOnTracker(); SettingsCache getSettingsCache(); + CustomWidgetManager getCustomWidgetManager(); /** Builder for LauncherBaseAppComponent. */ interface Builder { diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java index faa5d12875..0778172127 100644 --- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java +++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java @@ -33,7 +33,12 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.R; -import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.dagger.ApplicationContext; +import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.dagger.LauncherBaseAppComponent; +import com.android.launcher3.util.DaggerSingletonObject; +import com.android.launcher3.util.DaggerSingletonTracker; +import com.android.launcher3.util.ExecutorUtil; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.PluginManagerWrapper; import com.android.launcher3.util.SafeCloseable; @@ -50,13 +55,16 @@ import java.util.Map; import java.util.function.Consumer; import java.util.stream.Stream; +import javax.inject.Inject; + /** * CustomWidgetManager handles custom widgets implemented as a plugin. */ +@LauncherAppSingleton public class CustomWidgetManager implements PluginListener, SafeCloseable { - public static final MainThreadInitializedObject INSTANCE = - new MainThreadInitializedObject<>(CustomWidgetManager::new); + public static final DaggerSingletonObject INSTANCE = + new DaggerSingletonObject<>(LauncherBaseAppComponent::getCustomWidgetManager); private static final String TAG = "CustomWidgetManager"; private static final String PLUGIN_PKG = "android"; @@ -66,34 +74,44 @@ public class CustomWidgetManager implements PluginListener, private Consumer mWidgetRefreshCallback; private final @NonNull AppWidgetManager mAppWidgetManager; - private CustomWidgetManager(Context context) { - this(context, AppWidgetManager.getInstance(context)); + @Inject + CustomWidgetManager(@ApplicationContext Context context, DaggerSingletonTracker tracker) { + this(context, AppWidgetManager.getInstance(context), tracker); } @VisibleForTesting - CustomWidgetManager(Context context, @NonNull AppWidgetManager widgetManager) { + CustomWidgetManager(@ApplicationContext Context context, + @NonNull AppWidgetManager widgetManager, + DaggerSingletonTracker tracker) { mContext = context; mAppWidgetManager = widgetManager; mPlugins = new HashMap<>(); mCustomWidgets = new ArrayList<>(); - PluginManagerWrapper.INSTANCE.get(context) - .addPluginListener(this, CustomWidgetPlugin.class, true); - if (enableSmartspaceAsAWidget()) { - for (String s: context.getResources() - .getStringArray(R.array.custom_widget_providers)) { - try { - Class cls = Class.forName(s); - CustomWidgetPlugin plugin = (CustomWidgetPlugin) - cls.getDeclaredConstructor(Context.class).newInstance(context); - onPluginConnected(plugin, context); - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException - | ClassCastException | NoSuchMethodException - | InvocationTargetException e) { - Log.e(TAG, "Exception found when trying to add custom widgets: " + e); + + ExecutorUtil.executeSyncOnMainOrFail(() -> { + PluginManagerWrapper.INSTANCE.get(context) + .addPluginListener(this, CustomWidgetPlugin.class, true); + + if (enableSmartspaceAsAWidget()) { + for (String s: context.getResources() + .getStringArray(R.array.custom_widget_providers)) { + try { + Class cls = Class.forName(s); + CustomWidgetPlugin plugin = (CustomWidgetPlugin) + cls.getDeclaredConstructor(Context.class).newInstance(context); + onPluginConnected(plugin, context); + } catch (ClassNotFoundException | InstantiationException + | IllegalAccessException + | ClassCastException | NoSuchMethodException + | InvocationTargetException e) { + Log.e(TAG, "Exception found when trying to add custom widgets: " + e); + } } } - } + + tracker.addCloseable(this); + }); } @Override diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt index 4b5710d5ac..82f56b8c2e 100644 --- a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt @@ -23,6 +23,7 @@ import android.platform.test.flag.junit.SetFlagsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.util.DaggerSingletonTracker import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext import com.android.launcher3.util.PluginManagerWrapper import com.android.launcher3.util.WidgetUtils @@ -57,12 +58,13 @@ class CustomWidgetManagerTest { @Mock private lateinit var pluginManager: PluginManagerWrapper @Mock private lateinit var mockAppWidgetManager: AppWidgetManager + @Mock private lateinit var tracker: DaggerSingletonTracker @Before fun setUp() { MockitoAnnotations.initMocks(this) context.putObject(PluginManagerWrapper.INSTANCE, pluginManager) - underTest = CustomWidgetManager(context, mockAppWidgetManager) + underTest = CustomWidgetManager(context, mockAppWidgetManager, tracker) } @After From e4ce5ebc5e6bef6548aae611c5abcb09d31f5a38 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Mon, 14 Oct 2024 16:20:15 -0700 Subject: [PATCH 08/11] Fixing leak in LauncherAppWidgetProviderInfo Bug: 372998377 Test: Presubmit Flag: EXEMPT bugfix Change-Id: Ibf7c66f2aba510f86d23825816639bc3197884aa --- .../android/launcher3/widget/LauncherAppWidgetProviderInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java index fba3936be9..b877d7ab61 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java @@ -98,7 +98,7 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo impleme } public void initSpans(Context context, InvariantDeviceProfile idp) { - mPM = context.getPackageManager(); + mPM = context.getApplicationContext().getPackageManager(); int minSpanX = 0; int minSpanY = 0; int maxSpanX = idp.numColumns; From d82503fc42c6ae238d23f3ef287feb8f8172c7ce Mon Sep 17 00:00:00 2001 From: Sukesh Ram Date: Mon, 30 Sep 2024 17:17:10 -0700 Subject: [PATCH 09/11] Fix Taskbar Y-Translation with Visible Bottom Sheet When bottom sheet is visible, such as in SUW or when app launcher pauses, the taskbar three buttons are translated upwards clashing with SUW UI. Flag: EXEMPT bugfix Bug: 358532768 Test: Manual Change-Id: I0d18bda5cb7aeae894f5058df89cc413e34c00fc --- .../taskbar/LauncherTaskbarUIController.java | 45 ++++++++++++++++--- .../taskbar/NavbarButtonsViewController.java | 32 ++++++------- .../taskbar/TaskbarUIController.java | 8 ++++ .../taskbar/TaskbarViewController.java | 2 +- .../uioverrides/QuickstepLauncher.java | 6 +-- src/com/android/launcher3/BaseActivity.java | 4 ++ 6 files changed, 71 insertions(+), 26 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index 09dbeb6fb9..042bc9a8ab 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -36,6 +36,7 @@ import com.android.launcher3.Flags; import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.InstanceIdSequence; import com.android.launcher3.model.data.ItemInfo; @@ -68,14 +69,17 @@ public class LauncherTaskbarUIController extends TaskbarUIController { public static final int ALL_APPS_PAGE_PROGRESS_INDEX = 1; public static final int WIDGETS_PAGE_PROGRESS_INDEX = 2; public static final int SYSUI_SURFACE_PROGRESS_INDEX = 3; + public static final int LAUNCHER_PAUSE_PROGRESS_INDEX = 4; - public static final int DISPLAY_PROGRESS_COUNT = 4; + public static final int DISPLAY_PROGRESS_COUNT = 5; private final AnimatedFloat mTaskbarInAppDisplayProgress = new AnimatedFloat( this::onInAppDisplayProgressChanged); private final MultiPropertyFactory mTaskbarInAppDisplayProgressMultiProp = new MultiPropertyFactory<>(mTaskbarInAppDisplayProgress, AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max); + private final AnimatedFloat mLauncherPauseProgress = new AnimatedFloat( + this::launcherPauseProgressUpdate); private final QuickstepLauncher mLauncher; private final HomeVisibilityState mHomeState; @@ -190,6 +194,33 @@ public class LauncherTaskbarUIController extends TaskbarUIController { placeholderDuration)); } + /** + * Called when Launcher Activity is paused/resumed. + *

+ * To avoid UI clash between taskbar & bottom sheet, shift nav buttons down on launcher + * pause/resume at home. + * @param paused if launcher is currently paused. + */ + public void onLauncherPausedOrResumed(boolean paused) { + if (!FeatureFlags.enableHomeTransitionListener()) { + onLauncherVisibilityChanged(mLauncher.hasBeenResumed()); + return; + } + + // Animate navbar iff pause/resume from home, NOT to/from app (avoid overriding existing + // animations). + boolean launcherPauseOrResumeFromHome = mHomeState.isHomeVisible() && mControllers + .taskbarAutohideSuspendController.isSuspendedForTransientTaskbarInLauncher(); + if (launcherPauseOrResumeFromHome) { + mLauncherPauseProgress.animateToValue(paused ? 1.0f : 0.0f).start(); + } + } + + private void launcherPauseProgressUpdate() { + onTaskbarInAppDisplayProgressUpdate( + mLauncherPauseProgress.value, LAUNCHER_PAUSE_PROGRESS_INDEX); + } + /** * Should be called from onResume() and onPause(), and animates the Taskbar accordingly. */ @@ -364,18 +395,20 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } if (mControllers.uiController.isIconAlignedWithHotseat() && !mTaskbarLauncherStateController.isAnimatingToLauncher()) { - // Only animate the nav buttons while home and not animating home, otherwise let + // Only animate nav button position while home and not animating home, otherwise let // the TaskbarViewController handle it. mControllers.navbarButtonsViewController - .getTaskbarNavButtonTranslationYForInAppDisplay() + .getNavButtonTranslationYForInAppDisplay() .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY() * mTaskbarInAppDisplayProgress.value); - mControllers.navbarButtonsViewController - .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress); + if (!mLauncher.isPaused()) { + mControllers.navbarButtonsViewController + .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress); + } } } - /** Returns true iff any in-app display progress > 0. */ + @Override public boolean shouldUseInAppLayout() { return mTaskbarInAppDisplayProgress.value > 0; } diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java index 7d8e93ce00..cfcbd2f552 100644 --- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java @@ -183,7 +183,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT private final AnimatedFloat mTaskbarNavButtonTranslationY = new AnimatedFloat( this::updateNavButtonTranslationY); - private final AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay = new AnimatedFloat( + private final AnimatedFloat mNavButtonTranslationYForInAppDisplay = new AnimatedFloat( this::updateNavButtonTranslationY); private final AnimatedFloat mTaskbarNavButtonTranslationYForIme = new AnimatedFloat( this::updateNavButtonTranslationY); @@ -704,8 +704,8 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } /** Use to set the translationY for the all nav+contextual buttons when in Launcher */ - public AnimatedFloat getTaskbarNavButtonTranslationYForInAppDisplay() { - return mTaskbarNavButtonTranslationYForInAppDisplay; + public AnimatedFloat getNavButtonTranslationYForInAppDisplay() { + return mNavButtonTranslationYForInAppDisplay; } /** Use to set the dark intensity for the all nav+contextual buttons */ @@ -751,20 +751,22 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT if (mContext.isPhoneButtonNavMode()) { return; } - final float normalTranslationY = mTaskbarNavButtonTranslationY.value; - final float imeAdjustmentTranslationY = mTaskbarNavButtonTranslationYForIme.value; - TaskbarUIController uiController = mControllers.uiController; - final float inAppDisplayAdjustmentTranslationY = - (uiController instanceof LauncherTaskbarUIController - && ((LauncherTaskbarUIController) uiController).shouldUseInAppLayout()) - ? mTaskbarNavButtonTranslationYForInAppDisplay.value : 0; - - mLastSetNavButtonTranslationY = normalTranslationY - + imeAdjustmentTranslationY - + inAppDisplayAdjustmentTranslationY; + mLastSetNavButtonTranslationY = calculateNavButtonTranslationY(); mNavButtonsView.setTranslationY(mLastSetNavButtonTranslationY); } + /** + * Calculates the translationY of the nav buttons based on the current device state. + */ + private float calculateNavButtonTranslationY() { + float translationY = + mTaskbarNavButtonTranslationY.value + mTaskbarNavButtonTranslationYForIme.value; + if (mControllers.uiController.shouldUseInAppLayout()) { + translationY += mNavButtonTranslationYForInAppDisplay.value; + } + return translationY; + } + /** * Sets Taskbar 3-button mode icon colors based on the * {@link #mTaskbarNavButtonDarkIntensity} value piped in from Framework. For certain cases @@ -1162,7 +1164,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT pw.println(prefix + "\t\tmTaskbarNavButtonTranslationY=" + mTaskbarNavButtonTranslationY.value); pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForInAppDisplay=" - + mTaskbarNavButtonTranslationYForInAppDisplay.value); + + mNavButtonTranslationYForInAppDisplay.value); pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForIme=" + mTaskbarNavButtonTranslationYForIme.value); pw.println(prefix + "\t\tmTaskbarNavButtonDarkIntensity=" diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java index b80aaf8840..7030088287 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -90,6 +90,14 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio protected void onStashedInAppChanged() { } + /** + * Whether the Taskbar should use in-app layout. + * @return {@code true} iff in-app display progress > 0 or Launcher Activity paused. + */ + public boolean shouldUseInAppLayout() { + return false; + } + /** * Called when taskbar icon layout bounds change. */ diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index bc61c72212..c4d9e5028a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -234,7 +234,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar mTaskbarNavButtonTranslationY = controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY(); mTaskbarNavButtonTranslationYForInAppDisplay = controllers.navbarButtonsViewController - .getTaskbarNavButtonTranslationYForInAppDisplay(); + .getNavButtonTranslationYForInAppDisplay(); mActivity.addOnDeviceProfileChangeListener(mDeviceProfileChangeListener); diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 39bf6ac814..ce4e9807e5 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -419,10 +419,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer, mDepthController.setActivityStarted(isStarted()); } - if ((changeBits & ACTIVITY_STATE_RESUMED) != 0) { - if (!FeatureFlags.enableHomeTransitionListener() && mTaskbarUIController != null) { - mTaskbarUIController.onLauncherVisibilityChanged(hasBeenResumed()); - } + if ((changeBits & ACTIVITY_STATE_RESUMED) != 0 && mTaskbarUIController != null) { + mTaskbarUIController.onLauncherPausedOrResumed(isPaused()); } super.onActivityFlagsChanged(changeBits); diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java index 2e7526192d..3774ae3afe 100644 --- a/src/com/android/launcher3/BaseActivity.java +++ b/src/com/android/launcher3/BaseActivity.java @@ -306,6 +306,10 @@ public abstract class BaseActivity extends Activity implements ActivityContext { removeActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_DEFERRED_RESUMED); } + public boolean isPaused() { + return !hasBeenResumed() && (mActivityFlags & ACTIVITY_STATE_DEFERRED_RESUMED) == 0; + } + /** * Sets the activity to appear as resumed. */ From c33fe5bd562084c9d0d78f0c365a761accc8a9c7 Mon Sep 17 00:00:00 2001 From: Liran Binyamin Date: Thu, 10 Oct 2024 16:04:16 -0400 Subject: [PATCH 10/11] Update taskbar window size for flyout This change updates the taskbar window size after the flyout view has measured itself. This ensure that the taskbar window is tall enough to display the entire flyout. When the flyout is removed we reset the taskbar window size. Flag: com.android.wm.shell.enable_bubble_bar Bug: 277815200 Test: builds successfully and tested manually -- code is not wired up Change-Id: I5e8618e57443212e1c4f19ab20d1542ae2d1b865 --- .../bubbles/BubbleBarViewController.java | 21 +++ .../flyout/BubbleBarFlyoutController.kt | 48 ++++++- .../bubbles/flyout/BubbleBarFlyoutView.kt | 11 +- .../taskbar/bubbles/flyout/FlyoutScheduler.kt | 32 +++++ .../flyout/BubbleBarFlyoutControllerTest.kt | 128 ++++++++++++++---- 5 files changed, 206 insertions(+), 34 deletions(-) create mode 100644 quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 69e1d4359d..63f101fd51 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -43,6 +43,7 @@ import com.android.launcher3.taskbar.TaskbarControllers; import com.android.launcher3.taskbar.TaskbarInsetsController; import com.android.launcher3.taskbar.TaskbarStashController; import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator; +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController; import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner; import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; import com.android.launcher3.util.MultiPropertyFactory; @@ -117,6 +118,8 @@ public class BubbleBarViewController { public boolean mOverflowAdded; private BubbleBarViewAnimator mBubbleBarViewAnimator; + private final FrameLayout mBubbleBarContainer; + private BubbleBarFlyoutController mBubbleBarFlyoutController; private final TimeSource mTimeSource = System::currentTimeMillis; @@ -127,6 +130,7 @@ public class BubbleBarViewController { FrameLayout bubbleBarContainer) { mActivity = activity; mBarView = barView; + mBubbleBarContainer = bubbleBarContainer; mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity); mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */); mIconSize = activity.getResources().getDimensionPixelSize( @@ -141,6 +145,8 @@ public class BubbleBarViewController { mBubbleDragController = bubbleControllers.bubbleDragController; mTaskbarStashController = controllers.taskbarStashController; mTaskbarInsetsController = controllers.taskbarInsetsController; + mBubbleBarFlyoutController = new BubbleBarFlyoutController( + mBubbleBarContainer, createFlyoutPositioner(), createFlyoutTopBoundaryListener()); mBubbleBarViewAnimator = new BubbleBarViewAnimator( mBarView, mBubbleStashController, mBubbleBarController::showExpandedView); mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider; @@ -266,6 +272,21 @@ public class BubbleBarViewController { }; } + private BubbleBarFlyoutController.TopBoundaryListener createFlyoutTopBoundaryListener() { + return new BubbleBarFlyoutController.TopBoundaryListener() { + @Override + public void extendTopBoundary(int space) { + int defaultSize = mActivity.getDefaultTaskbarWindowSize(); + mActivity.setTaskbarWindowSize(defaultSize + space); + } + + @Override + public void resetTopBoundary() { + mActivity.setTaskbarWindowSize(mActivity.getDefaultTaskbarWindowSize()); + } + }; + } + private void onBubbleClicked(BubbleView bubbleView) { bubbleView.markSeen(); BubbleBarItem bubble = bubbleView.getBubble(); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt index 49760ff113..c431deb27c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt @@ -21,20 +21,30 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.animation.ValueAnimator import com.android.launcher3.R +import com.android.systemui.util.doOnEnd +import com.android.systemui.util.doOnStart /** Creates and manages the visibility of the [BubbleBarFlyoutView]. */ -class BubbleBarFlyoutController( +class BubbleBarFlyoutController +@JvmOverloads +constructor( private val container: FrameLayout, private val positioner: BubbleBarFlyoutPositioner, + private val topBoundaryListener: TopBoundaryListener, + private val flyoutScheduler: FlyoutScheduler = HandlerScheduler(container), ) { + private companion object { + const val EXPAND_COLLAPSE_ANIMATION_DURATION_MS = 250L + } + private var flyout: BubbleBarFlyoutView? = null private val horizontalMargin = container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin) fun setUpFlyout(message: BubbleBarFlyoutMessage) { flyout?.let(container::removeView) - val flyout = BubbleBarFlyoutView(container.context, positioner) + val flyout = BubbleBarFlyoutView(container.context, positioner, flyoutScheduler) flyout.translationY = positioner.targetTy @@ -48,17 +58,43 @@ class BubbleBarFlyoutController( lp.marginEnd = horizontalMargin container.addView(flyout, lp) - val animator = ValueAnimator.ofFloat(0f, 1f) + val animator = + ValueAnimator.ofFloat(0f, 1f).setDuration(EXPAND_COLLAPSE_ANIMATION_DURATION_MS) animator.addUpdateListener { _ -> flyout.updateExpansionProgress(animator.animatedValue as Float) } + animator.doOnStart { + val flyoutTop = flyout.top + flyout.translationY + // If the top position of the flyout is negative, then it's bleeding over the + // top boundary of its parent view + if (flyoutTop < 0) topBoundaryListener.extendTopBoundary(space = -flyoutTop.toInt()) + } flyout.showFromCollapsed(message) { animator.start() } this.flyout = flyout } - fun hideFlyout() { + fun hideFlyout(endAction: () -> Unit) { val flyout = this.flyout ?: return - container.removeView(flyout) - this.flyout = null + val animator = + ValueAnimator.ofFloat(1f, 0f).setDuration(EXPAND_COLLAPSE_ANIMATION_DURATION_MS) + animator.addUpdateListener { _ -> + flyout.updateExpansionProgress(animator.animatedValue as Float) + } + animator.doOnEnd { + container.removeView(flyout) + this@BubbleBarFlyoutController.flyout = null + topBoundaryListener.resetTopBoundary() + endAction() + } + animator.start() + } + + /** Notifies when the top boundary of the flyout view changes. */ + interface TopBoundaryListener { + /** Requests to extend the top boundary of the parent to fully include the flyout. */ + fun extendTopBoundary(space: Int) + + /** Resets the top boundary of the parent. */ + fun resetTopBoundary() } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt index 2022a427db..c60fba2e21 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt @@ -36,14 +36,18 @@ import com.android.launcher3.R import com.android.launcher3.popup.RoundedArrowDrawable /** The flyout view used to notify the user of a new bubble notification. */ -class BubbleBarFlyoutView(context: Context, private val positioner: BubbleBarFlyoutPositioner) : - ConstraintLayout(context) { +class BubbleBarFlyoutView( + context: Context, + private val positioner: BubbleBarFlyoutPositioner, + scheduler: FlyoutScheduler? = null, +) : ConstraintLayout(context) { private companion object { // the minimum progress of the expansion animation before the content starts fading in. const val MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA = 0.75f } + private val scheduler: FlyoutScheduler = scheduler ?: HandlerScheduler(this) private val title: TextView by lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_title) } @@ -197,11 +201,10 @@ class BubbleBarFlyoutView(context: Context, private val positioner: BubbleBarFly // post the request to start the expand animation to the looper so the view can measure // itself - post(expandAnimation) + scheduler.runAfterLayout(expandAnimation) } private fun setData(flyoutMessage: BubbleBarFlyoutMessage) { - // the avatar is only displayed in group chat messages if (flyoutMessage.icon != null) { icon.visibility = VISIBLE icon.setImageDrawable(flyoutMessage.icon) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt new file mode 100644 index 0000000000..6f5d700b0f --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt @@ -0,0 +1,32 @@ +/* + * 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.launcher3.taskbar.bubbles.flyout + +import android.view.View + +/** Interface for scheduling jobs by flyout. */ +fun interface FlyoutScheduler { + /** Runs the given [block] after layout. */ + fun runAfterLayout(block: () -> Unit) +} + +/** A [FlyoutScheduler] that uses a Handler to schedule jobs. */ +class HandlerScheduler(val view: View) : FlyoutScheduler { + override fun runAfterLayout(block: () -> Unit) { + view.post(block) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt index fdafce082b..3dd7689a4e 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt @@ -22,12 +22,15 @@ import android.graphics.PointF import android.view.Gravity import android.widget.FrameLayout import android.widget.TextView +import androidx.core.animation.AnimatorTestRule import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.R import com.google.common.truth.Truth.assertThat import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -36,11 +39,15 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BubbleBarFlyoutControllerTest { + @get:Rule val animatorTestRule = AnimatorTestRule() + private lateinit var flyoutController: BubbleBarFlyoutController private lateinit var flyoutContainer: FrameLayout + private lateinit var topBoundaryListener: FakeTopBoundaryListener private val context = ApplicationProvider.getApplicationContext() private val flyoutMessage = BubbleBarFlyoutMessage(icon = null, "sender name", "message") private var onLeft = true + private var flyoutTy = 50f @Before fun setUp() { @@ -50,53 +57,126 @@ class BubbleBarFlyoutControllerTest { override val isOnLeft get() = onLeft - override val targetTy = 50f + override val targetTy + get() = flyoutTy + override val distanceToCollapsedPosition = PointF(100f, 200f) override val collapsedSize = 30f override val collapsedColor = Color.BLUE override val collapsedElevation = 1f override val distanceToRevealTriangle = 50f } - flyoutController = BubbleBarFlyoutController(flyoutContainer, positioner) + topBoundaryListener = FakeTopBoundaryListener() + val flyoutScheduler = FlyoutScheduler { block -> block.invoke() } + flyoutController = + BubbleBarFlyoutController( + flyoutContainer, + positioner, + topBoundaryListener, + flyoutScheduler, + ) } @Test fun flyoutPosition_left() { - flyoutController.setUpFlyout(flyoutMessage) - assertThat(flyoutContainer.childCount).isEqualTo(1) - val flyout = flyoutContainer.getChildAt(0) - val lp = flyout.layoutParams as FrameLayout.LayoutParams - assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.LEFT) - assertThat(flyout.translationY).isEqualTo(50f) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val lp = flyout.layoutParams as FrameLayout.LayoutParams + assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.LEFT) + assertThat(flyout.translationY).isEqualTo(50f) + } } @Test fun flyoutPosition_right() { onLeft = false - flyoutController.setUpFlyout(flyoutMessage) - assertThat(flyoutContainer.childCount).isEqualTo(1) - val flyout = flyoutContainer.getChildAt(0) - val lp = flyout.layoutParams as FrameLayout.LayoutParams - assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.RIGHT) - assertThat(flyout.translationY).isEqualTo(50f) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val lp = flyout.layoutParams as FrameLayout.LayoutParams + assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.RIGHT) + assertThat(flyout.translationY).isEqualTo(50f) + } } @Test fun flyoutMessage() { - flyoutController.setUpFlyout(flyoutMessage) - assertThat(flyoutContainer.childCount).isEqualTo(1) - val flyout = flyoutContainer.getChildAt(0) - val sender = flyout.findViewById(R.id.bubble_flyout_title) - assertThat(sender.text).isEqualTo("sender name") - val message = flyout.findViewById(R.id.bubble_flyout_text) - assertThat(message.text).isEqualTo("message") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val sender = flyout.findViewById(R.id.bubble_flyout_title) + assertThat(sender.text).isEqualTo("sender name") + val message = flyout.findViewById(R.id.bubble_flyout_text) + assertThat(message.text).isEqualTo("message") + } } @Test fun hideFlyout_removedFromContainer() { - flyoutController.setUpFlyout(flyoutMessage) - assertThat(flyoutContainer.childCount).isEqualTo(1) - flyoutController.hideFlyout() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + flyoutController.hideFlyout {} + animatorTestRule.advanceTimeBy(300) + } assertThat(flyoutContainer.childCount).isEqualTo(0) } + + @Test + fun showFlyout_extendsTopBoundary() { + // set negative translation for the flyout so that it will request to extend the top + // boundary + flyoutTy = -50f + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(300) + } + assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(50) + } + + @Test + fun showFlyout_withinBoundary() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(300) + } + assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(0) + } + + @Test + fun hideFlyout_resetsTopBoundary() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + flyoutController.hideFlyout {} + animatorTestRule.advanceTimeBy(300) + } + assertThat(topBoundaryListener.topBoundaryReset).isTrue() + } + + class FakeTopBoundaryListener : BubbleBarFlyoutController.TopBoundaryListener { + + var topBoundaryExtendedSpace = 0 + var topBoundaryReset = false + + override fun extendTopBoundary(space: Int) { + topBoundaryExtendedSpace = space + } + + override fun resetTopBoundary() { + topBoundaryReset = true + } + } } From 44854ed9d5924bd3992bf605674d658fadd91c77 Mon Sep 17 00:00:00 2001 From: Federico Baron Date: Tue, 15 Oct 2024 16:11:10 +0000 Subject: [PATCH 11/11] Revert "Add screenrecord for testWorkspaceSwitchToAllApps" This reverts commit 1d12cbcd23a0a5b1c328061a7ad259fd374bfd09. Reason for revert: bug resolved Change-Id: Ie5a15594cb3b168c5368f6bd54aab2c6a9d718ae --- .../tests/src/com/android/quickstep/TaplTestsQuickstep.java | 1 - 1 file changed, 1 deletion(-) diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java index e7e2f577ce..5ff2af705b 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java @@ -103,7 +103,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { @Test @NavigationModeSwitch @PortraitLandscape - @ScreenRecord // b/371615571 public void testWorkspaceSwitchToAllApps() { assertNotNull("switchToAllApps() returned null", mLauncher.getWorkspace().switchToAllApps());