Merge "Made the taskbar touchable during animation to home." into main
diff --git a/quickstep/res/drawable/ic_external_display.xml b/quickstep/res/drawable/ic_external_display.xml
new file mode 100644
index 0000000..64c183e
--- /dev/null
+++ b/quickstep/res/drawable/ic_external_display.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:pathData="M320,840v-80L160,760q-33,0 -56.5,-23.5T80,680v-480q0,-33 23.5,-56.5T160,120h640q33,0 56.5,23.5T880,200v480q0,33 -23.5,56.5T800,760L640,760v80L320,840ZM160,680h640v-480L160,200v480ZM160,680v-480,480Z"
+ android:fillColor="#e8eaed"/>
+</vector>
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index e8cb5d5..41b2384 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -37,8 +37,8 @@
<string name="taskbar_edu_tooltip_controller_class" translatable="false">com.android.launcher3.taskbar.TaskbarEduTooltipController</string>
<string name="contextual_edu_manager_class" translatable="false">com.android.quickstep.contextualeducation.SystemContextualEduStatsManager</string>
<string name="nav_handle_long_press_handler_class" translatable="false"></string>
- <string name="assist_utils_class" translatable="false"></string>
- <string name="assist_state_manager_class" translatable="false"></string>
+ <string name="contextual_search_invoker_class" translatable="false"></string>
+ <string name="contextual_search_state_manager_class" translatable="false"></string>
<string name="api_wrapper_class" translatable="false">com.android.launcher3.uioverrides.SystemApiWrapper</string>
<!-- The number of thumbnails and icons to keep in the cache. The thumbnail cache size also
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 008766b..026e25c 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -28,6 +28,8 @@
<string name="recent_task_option_freeform">Freeform</string>
<!-- Title and content description for an option to enter desktop windowing mode for a given app -->
<string name="recent_task_option_desktop">Desktop</string>
+ <!-- Title and content description for an option to move app to external display. -->
+ <string name="recent_task_option_external_display">Move to external display</string>
<!-- Title and content description for Desktop tile in Recents screen that contains apps opened inside desktop windowing mode [CHAR LIMIT=NONE] -->
<string name="recent_task_desktop">Desktop</string>
@@ -360,4 +362,8 @@
<string name="bubble_bar_accessibility_announce_expand">expand <xliff:g id="bubble_description" example="some title from Messages">%1$s</xliff:g></string>
<!-- Accessibility announcement when the bubble bar collapses. [CHAR LIMIT=NONE]-->
<string name="bubble_bar_accessibility_announce_collapse">collapse <xliff:g id="bubble_description" example="some title from Messages">%1$s</xliff:g></string>
+
+ <!-- Name of Google's new feature to circle to search anything on your phone screen, without
+ switching apps. [CHAR_LIMIT=60] -->
+ <string name="search_gesture_feature_title">Circle to Search</string>
</resources>
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
index 8b3a032..ac1ffa6 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
@@ -66,6 +66,11 @@
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/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index 4014f06..29e1f4e 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.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 @@
@Override
public void workspaceLoadComplete() {
super.workspaceLoadComplete();
+ // Initialize ContextualSearchStateManager.
+ ContextualSearchStateManager.INSTANCE.get(mContext);
recreatePredictors();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 61563b9..0b850bd 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.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 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<AnimatedFloat> 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;
@@ -185,6 +189,33 @@
}
/**
+ * Called when Launcher Activity is paused/resumed.
+ * <p>
+ * 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.
*/
@Override
@@ -358,18 +389,20 @@
}
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 7d8e93c..cfcbd2f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -183,7 +183,7 @@
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 @@
}
/** 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,21 +751,23 @@
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
* in large screen taskbar where there may be opaque surfaces, the selected SystemUI button
@@ -1162,7 +1164,7 @@
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/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index 06376d3..ade8f8c 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.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 @@
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 @@
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 @@
override fun performAccessibilityAction(
host: View,
action: Int,
- args: Bundle?
+ args: Bundle?,
): Boolean {
if (action == R.id.close) {
hide()
@@ -396,13 +401,13 @@
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 @@
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 f2f6500..c18cf28 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -71,7 +71,7 @@
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;
@@ -249,7 +249,7 @@
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 15c35b6..8947914 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.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 @@
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 @@
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 @@
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/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 5c51fda..db69e8f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -97,6 +97,14 @@
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.
*/
protected void onIconLayoutBoundsChanged() { }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 435ee5b..f8adf8e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -442,13 +442,33 @@
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;
@@ -518,8 +538,6 @@
}
}
- updateRecentAppsToFit();
-
if (mActivityContext.getDeviceProfile().isQsbInline) {
addView(mQsb, mIsRtl ? getChildCount() : 0);
// Always set QSB to invisible after re-adding.
@@ -527,45 +545,6 @@
}
}
- /**
- * 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.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 176be1c..8bc1e12 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -19,6 +19,7 @@
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 @@
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 ba0f5a0..704d6cf 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 @@
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/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index bc61c72..c4d9e50 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -234,7 +234,7 @@
mTaskbarNavButtonTranslationY =
controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY();
mTaskbarNavButtonTranslationYForInAppDisplay = controllers.navbarButtonsViewController
- .getTaskbarNavButtonTranslationYForInAppDisplay();
+ .getNavButtonTranslationYForInAppDisplay();
mActivity.addOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 69e1d43..63f101f 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.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 boolean mOverflowAdded;
private BubbleBarViewAnimator mBubbleBarViewAnimator;
+ private final FrameLayout mBubbleBarContainer;
+ private BubbleBarFlyoutController mBubbleBarFlyoutController;
private final TimeSource mTimeSource = System::currentTimeMillis;
@@ -127,6 +130,7 @@
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 @@
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 @@
};
}
+ 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 49760ff..c431deb 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.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 @@
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 2022a42..c60fba2 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.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 @@
// 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 0000000..6f5d700
--- /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/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
index e6c0b2f..c5f8aa0 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.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 @@
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/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 39bf6ac..ce4e980 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -419,10 +419,8 @@
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/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt b/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt
new file mode 100644
index 0000000..46c4f36
--- /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<RecentsViewContainer>(
+ 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<*, *>>()
+ 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<ExternalDisplaySystemShortcut>? {
+ 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/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 461f963..e23947b 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.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 @@
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 @@
get() = overviewComponentObserver.containerInterface
private val visibleRecentsView: RecentsView<*, *>?
- get() = containerInterface.getVisibleRecentsView<RecentsView<*, *>>()
+ get() = containerInterface.getVisibleRecentsView()
/**
* Adds a command to be executed next, after all pending tasks are completed. Max commands that
@@ -105,11 +107,7 @@
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/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 5131774..de8be50 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.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;
@@ -70,7 +71,7 @@
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 +102,7 @@
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 +153,7 @@
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) {
@@ -563,6 +564,7 @@
return mAssistantAvailable
&& !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)
&& mRotationTouchHelper.touchInAssistantRegion(ev)
+ && !isTrackpadScroll(ev)
&& !isLockToAppActive();
}
@@ -617,8 +619,9 @@
: 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 7a0d701..c66813f 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.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 @@
setBackToLauncherCallback(mBackToLauncherCallback, mBackToLauncherRunner);
setUnfoldAnimationListener(mUnfoldAnimationListener);
setDesktopTaskListener(mDesktopTaskListener);
- setAssistantOverridesRequested(
- AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes());
+ setAssistantOverridesRequested(ContextualSearchInvoker.newInstance(mContext)
+ .getSysUiAssistOverrideInvocationTypes());
mStateChangeCallbacks.forEach(Runnable::run);
if (mUnfoldTransitionProvider != null) {
@@ -1493,6 +1493,17 @@
}
}
+ /** 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 8e45767..0dbdcb7 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -116,6 +116,7 @@
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/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index f0943dc..41a8a31 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.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 @@
@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 @@
}
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 341c868..335161b 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.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 @@
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 1d00e53..1a825a4 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 @@
*
* @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 @@
*
* @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 @@
* @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 f4d3695..f5bef05e 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.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 @@
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 7acb28d..0000000
--- 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<AssistStateManager> 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<Long> getLPNHDurationMillis() {
- return Optional.empty();
- }
-
- /**
- * Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant.
- */
- public Optional<Float> getLPNHCustomSlopMultiplier() {
- return Optional.empty();
- }
-
- /** Get the Launcher overridden long press home duration to trigger Assistant. */
- public Optional<Long> getLPHDurationMillis() {
- return Optional.empty();
- }
-
- /** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */
- public Optional<Float> 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<Boolean> 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 11b6ea7..0000000
--- 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 0000000..8c246a5
--- /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 0000000..dcb72aa
--- /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 0000000..142fc58
--- /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<ContextualSearchStateManager> 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<Long> getLPNHDurationMillis() {
+ return Optional.empty();
+ }
+
+ /**
+ * Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant.
+ */
+ public Optional<Float> getLPNHCustomSlopMultiplier() {
+ return Optional.empty();
+ }
+
+ /** Get the Launcher overridden long press home duration to trigger Assistant. */
+ public Optional<Long> getLPHDurationMillis() {
+ return Optional.empty();
+ }
+
+ /** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */
+ public Optional<Float> 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<Boolean> 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/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 7554c44..bccfa0c 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -6673,6 +6673,26 @@
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/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt
index a57fb70..6e2f74a 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 <T : Any> ConstantItem<T>.asProperty(context: Context): ReadWriteProperty<Any?, T> {
+ return TaskbarItemProperty(context, this)
+ }
+
+ private class TaskbarItemProperty<T : Any>(
+ private val context: Context,
+ private val item: ConstantItem<T>,
+ ) : ReadWriteProperty<Any?, T> {
+
+ 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 af05d22..e60717b 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 @@
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 @@
@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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
index 02d6218..253d921 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.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 @@
@Mock
Handler mockHandler;
@Mock
- AssistUtils mockAssistUtils;
+ ContextualSearchInvoker mockContextualSearchInvoker;
@Mock
StatsLogManager mockStatsLogManager;
@Mock
@@ -109,7 +109,7 @@
mockSystemUiProxy,
mockContextualEduStatsManager,
mockHandler,
- mockAssistUtils);
+ mockContextualSearchInvoker);
}
@Test
@@ -166,40 +166,40 @@
@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/launcher3/taskbar/TaskbarStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
index de73ce7..71f4ef4 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 @@
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.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 @@
@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 @@
@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 516220a..3c0d9c6 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.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.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 @@
@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 @@
@Test
fun testShow_firstAllAppsVisit_hasAllAppsTextDivider() {
- allAppsVisitedPreferenceRule.value = 0
+ allAppsVisitedCount = 0
val viewController = createViewController()
getInstrumentation().runOnMainSync { viewController.show(false) }
@@ -120,7 +118,7 @@
@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/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
index fdafce0..3dd7689 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.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 @@
@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<Context>()
private val flyoutMessage = BubbleBarFlyoutMessage(icon = null, "sender name", "message")
private var onLeft = true
+ private var flyoutTy = 50f
@Before
fun setUp() {
@@ -50,53 +57,126 @@
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<TextView>(R.id.bubble_flyout_title)
- assertThat(sender.text).isEqualTo("sender name")
- val message = flyout.findViewById<TextView>(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<TextView>(R.id.bubble_flyout_title)
+ assertThat(sender.text).isEqualTo("sender name")
+ val message = flyout.findViewById<TextView>(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
+ }
+ }
}
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 d417790..0000000
--- 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 977e7a5..0000000
--- 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 e42ca9e..0000000
--- 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<T : Any>(
- private val context: TaskbarWindowSandboxContext,
- private val constantItem: ConstantItem<T>,
-) : 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 b7e6fa3..0000000
--- 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 7728504..2d3bfd6 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.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 @@
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/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
index 0ae710f..b0db737 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.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 @@
private val testScope = TestScope(dispatcher)
private var pendingCallbacksWithDelays = mutableListOf<Long>()
+ private lateinit var pendingCommandsToExecute: MutableList<Runnable>
@Suppress("UNCHECKED_CAST")
@Before
fun setup() {
+ pendingCommandsToExecute = mutableListOf()
setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT)
sut =
@@ -68,7 +69,8 @@
touchInteractionService = mock(),
overviewComponentObserver = mock(),
taskAnimationManager = mock(),
- dispatcherProvider = TestDispatcherProvider(dispatcher)
+ dispatcherProvider = TestDispatcherProvider(dispatcher),
+ uiExecutor = { runnable -> pendingCommandsToExecute += runnable },
)
)
@@ -94,12 +96,21 @@
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 @@
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 @@
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 @@
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)
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 0000000..9018775
--- /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 0000000..543ffe6
--- /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/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
new file mode 100644
index 0000000..8968b9c
--- /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<LauncherRecentsView>()).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<Runnable>(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<TaskThumbnailView>()
+ else mock<TaskThumbnailViewDeprecated>()
+ return TaskContainer(
+ taskView,
+ task,
+ snapshotView,
+ iconView,
+ transformingTouchDelegate,
+ SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+ digitalWellBeingToast = null,
+ showWindowsView = null,
+ overlayFactory,
+ )
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index e7e2f57..5ff2af7 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -103,7 +103,6 @@
@Test
@NavigationModeSwitch
@PortraitLandscape
- @ScreenRecord // b/371615571
public void testWorkspaceSwitchToAllApps() {
assertNotNull("switchToAllApps() returned null",
mLauncher.getWorkspace().switchToAllApps());
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 2e75261..3774ae3 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -306,6 +306,10 @@
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.
*/
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 088277b..c3508b7 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.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 @@
InstallSessionHelper getInstallSessionHelper();
ScreenOnTracker getScreenOnTracker();
SettingsCache getSettingsCache();
+ CustomWidgetManager getCustomWidgetManager();
/** Builder for LauncherBaseAppComponent. */
interface Builder {
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index fbd24d8..e5cd76a 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -221,6 +221,9 @@
@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),
@@ -798,6 +801,44 @@
@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 +869,10 @@
@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;
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
index fba3936..b877d7a 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
@@ -98,7 +98,7 @@
}
public void initSpans(Context context, InvariantDeviceProfile idp) {
- mPM = context.getPackageManager();
+ mPM = context.getApplicationContext().getPackageManager();
int minSpanX = 0;
int minSpanY = 0;
int maxSpanX = idp.numColumns;
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
index faa5d12..0778172 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.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.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<CustomWidgetPlugin>, SafeCloseable {
- public static final MainThreadInitializedObject<CustomWidgetManager> INSTANCE =
- new MainThreadInitializedObject<>(CustomWidgetManager::new);
+ public static final DaggerSingletonObject<CustomWidgetManager> INSTANCE =
+ new DaggerSingletonObject<>(LauncherBaseAppComponent::getCustomWidgetManager);
private static final String TAG = "CustomWidgetManager";
private static final String PLUGIN_PKG = "android";
@@ -66,34 +74,44 @@
private Consumer<PackageUserKey> 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/FakeLauncherPrefs.kt b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt
index 058ac05..946bbc5 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 @@
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 b8e347c..2463c93 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 @@
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 @@
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 @@
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()
}
}
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 4b5710d..82f56b8 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 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 @@
@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