diff --git a/quickstep/res/drawable/ic_external_display.xml b/quickstep/res/drawable/ic_external_display.xml new file mode 100644 index 0000000000..64c183ea12 --- /dev/null +++ b/quickstep/res/drawable/ic_external_display.xml @@ -0,0 +1,24 @@ + + + + diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml index e8cb5d5241..41b2384a35 100644 --- a/quickstep/res/values/config.xml +++ b/quickstep/res/values/config.xml @@ -37,8 +37,8 @@ com.android.launcher3.taskbar.TaskbarEduTooltipController com.android.quickstep.contextualeducation.SystemContextualEduStatsManager - - + + com.android.launcher3.uioverrides.SystemApiWrapper Desktop + + Move to external display Desktop @@ -360,4 +362,8 @@ expand %1$s collapse %1$s + + + Circle to Search diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt index 8b3a032137..ac1ffa6029 100644 --- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt +++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt @@ -66,6 +66,11 @@ class DesktopRecentsTransitionController( systemUiProxy.moveToDesktop(taskId, transitionSource) } + /** Move task to external display from recents view */ + fun moveToExternalDisplay(taskId: Int) { + systemUiProxy.moveToExternalDisplay(taskId) + } + private class RemoteDesktopLaunchTransitionRunner( private val desktopTaskView: DesktopTaskView, private val animated: Boolean, diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java index 4014f069cc..29e1f4e409 100644 --- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java +++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java @@ -77,6 +77,7 @@ import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PersistedItemArray; import com.android.quickstep.logging.SettingsChangeLogger; import com.android.quickstep.logging.StatsLogCompatManager; +import com.android.quickstep.util.ContextualSearchStateManager; import com.android.systemui.shared.system.SysUiStatsLog; import java.util.ArrayList; @@ -209,6 +210,8 @@ public class QuickstepModelDelegate extends ModelDelegate { @Override public void workspaceLoadComplete() { super.workspaceLoadComplete(); + // Initialize ContextualSearchStateManager. + ContextualSearchStateManager.INSTANCE.get(mContext); recreatePredictors(); } diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index 09dbeb6fb9..042bc9a8ab 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -36,6 +36,7 @@ import com.android.launcher3.Flags; import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.InstanceIdSequence; import com.android.launcher3.model.data.ItemInfo; @@ -68,14 +69,17 @@ public class LauncherTaskbarUIController extends TaskbarUIController { public static final int ALL_APPS_PAGE_PROGRESS_INDEX = 1; public static final int WIDGETS_PAGE_PROGRESS_INDEX = 2; public static final int SYSUI_SURFACE_PROGRESS_INDEX = 3; + public static final int LAUNCHER_PAUSE_PROGRESS_INDEX = 4; - public static final int DISPLAY_PROGRESS_COUNT = 4; + public static final int DISPLAY_PROGRESS_COUNT = 5; private final AnimatedFloat mTaskbarInAppDisplayProgress = new AnimatedFloat( this::onInAppDisplayProgressChanged); private final MultiPropertyFactory mTaskbarInAppDisplayProgressMultiProp = new MultiPropertyFactory<>(mTaskbarInAppDisplayProgress, AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max); + private final AnimatedFloat mLauncherPauseProgress = new AnimatedFloat( + this::launcherPauseProgressUpdate); private final QuickstepLauncher mLauncher; private final HomeVisibilityState mHomeState; @@ -190,6 +194,33 @@ public class LauncherTaskbarUIController extends TaskbarUIController { placeholderDuration)); } + /** + * Called when Launcher Activity is paused/resumed. + *

+ * To avoid UI clash between taskbar & bottom sheet, shift nav buttons down on launcher + * pause/resume at home. + * @param paused if launcher is currently paused. + */ + public void onLauncherPausedOrResumed(boolean paused) { + if (!FeatureFlags.enableHomeTransitionListener()) { + onLauncherVisibilityChanged(mLauncher.hasBeenResumed()); + return; + } + + // Animate navbar iff pause/resume from home, NOT to/from app (avoid overriding existing + // animations). + boolean launcherPauseOrResumeFromHome = mHomeState.isHomeVisible() && mControllers + .taskbarAutohideSuspendController.isSuspendedForTransientTaskbarInLauncher(); + if (launcherPauseOrResumeFromHome) { + mLauncherPauseProgress.animateToValue(paused ? 1.0f : 0.0f).start(); + } + } + + private void launcherPauseProgressUpdate() { + onTaskbarInAppDisplayProgressUpdate( + mLauncherPauseProgress.value, LAUNCHER_PAUSE_PROGRESS_INDEX); + } + /** * Should be called from onResume() and onPause(), and animates the Taskbar accordingly. */ @@ -364,18 +395,20 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } if (mControllers.uiController.isIconAlignedWithHotseat() && !mTaskbarLauncherStateController.isAnimatingToLauncher()) { - // Only animate the nav buttons while home and not animating home, otherwise let + // Only animate nav button position while home and not animating home, otherwise let // the TaskbarViewController handle it. mControllers.navbarButtonsViewController - .getTaskbarNavButtonTranslationYForInAppDisplay() + .getNavButtonTranslationYForInAppDisplay() .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY() * mTaskbarInAppDisplayProgress.value); - mControllers.navbarButtonsViewController - .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress); + if (!mLauncher.isPaused()) { + mControllers.navbarButtonsViewController + .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress); + } } } - /** Returns true iff any in-app display progress > 0. */ + @Override public boolean shouldUseInAppLayout() { return mTaskbarInAppDisplayProgress.value > 0; } diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java index 7d8e93ce00..cfcbd2f552 100644 --- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java @@ -183,7 +183,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT private final AnimatedFloat mTaskbarNavButtonTranslationY = new AnimatedFloat( this::updateNavButtonTranslationY); - private final AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay = new AnimatedFloat( + private final AnimatedFloat mNavButtonTranslationYForInAppDisplay = new AnimatedFloat( this::updateNavButtonTranslationY); private final AnimatedFloat mTaskbarNavButtonTranslationYForIme = new AnimatedFloat( this::updateNavButtonTranslationY); @@ -704,8 +704,8 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } /** Use to set the translationY for the all nav+contextual buttons when in Launcher */ - public AnimatedFloat getTaskbarNavButtonTranslationYForInAppDisplay() { - return mTaskbarNavButtonTranslationYForInAppDisplay; + public AnimatedFloat getNavButtonTranslationYForInAppDisplay() { + return mNavButtonTranslationYForInAppDisplay; } /** Use to set the dark intensity for the all nav+contextual buttons */ @@ -751,20 +751,22 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT if (mContext.isPhoneButtonNavMode()) { return; } - final float normalTranslationY = mTaskbarNavButtonTranslationY.value; - final float imeAdjustmentTranslationY = mTaskbarNavButtonTranslationYForIme.value; - TaskbarUIController uiController = mControllers.uiController; - final float inAppDisplayAdjustmentTranslationY = - (uiController instanceof LauncherTaskbarUIController - && ((LauncherTaskbarUIController) uiController).shouldUseInAppLayout()) - ? mTaskbarNavButtonTranslationYForInAppDisplay.value : 0; - - mLastSetNavButtonTranslationY = normalTranslationY - + imeAdjustmentTranslationY - + inAppDisplayAdjustmentTranslationY; + mLastSetNavButtonTranslationY = calculateNavButtonTranslationY(); mNavButtonsView.setTranslationY(mLastSetNavButtonTranslationY); } + /** + * Calculates the translationY of the nav buttons based on the current device state. + */ + private float calculateNavButtonTranslationY() { + float translationY = + mTaskbarNavButtonTranslationY.value + mTaskbarNavButtonTranslationYForIme.value; + if (mControllers.uiController.shouldUseInAppLayout()) { + translationY += mNavButtonTranslationYForInAppDisplay.value; + } + return translationY; + } + /** * Sets Taskbar 3-button mode icon colors based on the * {@link #mTaskbarNavButtonDarkIntensity} value piped in from Framework. For certain cases @@ -1162,7 +1164,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT pw.println(prefix + "\t\tmTaskbarNavButtonTranslationY=" + mTaskbarNavButtonTranslationY.value); pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForInAppDisplay=" - + mTaskbarNavButtonTranslationYForInAppDisplay.value); + + mNavButtonTranslationYForInAppDisplay.value); pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForIme=" + mTaskbarNavButtonTranslationYForIme.value); pw.println(prefix + "\t\tmTaskbarNavButtonDarkIntensity=" diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt index 06376d3fd2..ade8f8cdc6 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt @@ -49,6 +49,7 @@ import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN import com.android.launcher3.util.ResourceBasedOverride import com.android.launcher3.views.ActivityContext import com.android.launcher3.views.BaseDragLayer +import com.android.quickstep.util.ContextualSearchInvoker import com.android.quickstep.util.LottieAnimationColorUtils import java.io.PrintWriter @@ -80,7 +81,11 @@ open class TaskbarEduTooltipController(context: Context) : ResourceBasedOverride, LoggableTaskbarController { protected val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context) - open val shouldShowSearchEdu = false + open val shouldShowSearchEdu: Boolean + get() = + ContextualSearchInvoker.newInstance(activityContext) + .runContextualSearchInvocationChecksAndLogFailures() + private val isTooltipEnabled: Boolean get() { return !Utilities.isRunningInTestHarness() && @@ -351,19 +356,19 @@ open class TaskbarEduTooltipController(context: Context) : overlayContext.layoutInflater.inflate( R.layout.taskbar_edu_tooltip, overlayContext.dragLayer, - false + false, ) as TaskbarEduTooltip controllers.taskbarAutohideSuspendController.updateFlag( FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, - true + true, ) tooltip.onCloseCallback = { this.tooltip = null controllers.taskbarAutohideSuspendController.updateFlag( FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, - false + false, ) controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true) } @@ -378,7 +383,7 @@ open class TaskbarEduTooltipController(context: Context) : override fun performAccessibilityAction( host: View, action: Int, - args: Bundle? + args: Bundle?, ): Boolean { if (action == R.id.close) { hide() @@ -396,13 +401,13 @@ open class TaskbarEduTooltipController(context: Context) : override fun onInitializeAccessibilityNodeInfo( host: View, - info: AccessibilityNodeInfo + info: AccessibilityNodeInfo, ) { super.onInitializeAccessibilityNodeInfo(host, info) info.addAction( AccessibilityNodeInfo.AccessibilityAction( R.id.close, - host.context?.getText(R.string.taskbar_edu_close) + host.context?.getText(R.string.taskbar_edu_close), ) ) } @@ -421,7 +426,7 @@ open class TaskbarEduTooltipController(context: Context) : return ResourceBasedOverride.Overrides.getObject( TaskbarEduTooltipController::class.java, context, - R.string.taskbar_edu_tooltip_controller_class + R.string.taskbar_edu_tooltip_controller_class, ) } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java index f2f650000a..c18cf28996 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.launcher3.util.SimpleBroadcastReceiver; import com.android.quickstep.AllAppsActionManager; import com.android.quickstep.RecentsActivity; import com.android.quickstep.SystemUiProxy; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; import com.android.systemui.shared.statusbar.phone.BarTransitions; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; @@ -249,7 +249,7 @@ public class TaskbarManager { SystemUiProxy.INSTANCE.get(mContext), ContextualEduStatsManager.INSTANCE.get(mContext), new Handler(), - AssistUtils.newInstance(mContext)); + ContextualSearchInvoker.newInstance(mContext)); mComponentCallbacks = new ComponentCallbacks() { private Configuration mOldConfig = mContext.getResources().getConfiguration(); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java index 15c35b644e..894791479e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java @@ -51,7 +51,7 @@ import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; @@ -113,7 +113,7 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa private final SystemUiProxy mSystemUiProxy; private final ContextualEduStatsManager mContextualEduStatsManager; private final Handler mHandler; - private final AssistUtils mAssistUtils; + private final ContextualSearchInvoker mContextualSearchInvoker; @Nullable private StatsLogManager mStatsLogManager; private final Runnable mResetLongPress = this::resetScreenUnpin; @@ -124,13 +124,13 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa SystemUiProxy systemUiProxy, ContextualEduStatsManager contextualEduStatsManager, Handler handler, - AssistUtils assistUtils) { + ContextualSearchInvoker contextualSearchInvoker) { mContext = context; mCallbacks = callbacks; mSystemUiProxy = systemUiProxy; mContextualEduStatsManager = contextualEduStatsManager; mHandler = handler; - mAssistUtils = assistUtils; + mContextualSearchInvoker = contextualSearchInvoker; } public void onButtonClick(@TaskbarButton int buttonType, View view) { @@ -344,8 +344,9 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa if (mScreenPinned || !mAssistantLongPressEnabled) { return; } - // Attempt to start Assist with AssistUtils, otherwise fall back to SysUi's implementation. - if (!mAssistUtils.tryStartAssistOverride(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) { + // Attempt to start Contextual Search, otherwise fall back to SysUi's implementation. + if (!mContextualSearchInvoker.tryStartAssistOverride( + INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) { Bundle args = new Bundle(); args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); mSystemUiProxy.startAssistant(args); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java index b80aaf8840..7030088287 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -90,6 +90,14 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio protected void onStashedInAppChanged() { } + /** + * Whether the Taskbar should use in-app layout. + * @return {@code true} iff in-app display progress > 0 or Launcher Activity paused. + */ + public boolean shouldUseInAppLayout() { + return false; + } + /** * Called when taskbar icon layout bounds change. */ diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index ec9416017e..70dcfe8cf9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -443,13 +443,33 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) { addView(mTaskbarDividerContainer, nextViewIndex++); mAddedDividerForRecents = true; - if (mTaskbarOverflowView != null) { + } + + // At this point, the all apps button has not been added as a child view, but needs to be + // accounted for when comparing current icon count to max number of icons. + int nonTaskIconsToBeAdded = 1; + + boolean supportsOverflow = Flags.taskbarOverflow(); + if (supportsOverflow) { + int numberOfSupportedRecents = 0; + for (GroupTask task : recentTasks) { + // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. + if (!task.hasMultipleTasks()) { + ++numberOfSupportedRecents; + } + } + if (nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded > mMaxNumIcons + && mTaskbarOverflowView != null) { addView(mTaskbarOverflowView, nextViewIndex++); } } // Add Recent/Running icons. for (GroupTask task : recentTasks) { + if (supportsOverflow && nextViewIndex + nonTaskIconsToBeAdded >= mMaxNumIcons) { + break; + } + // Replace any Recent views with the appropriate type if it's not already that type. final int expectedLayoutResId; boolean isCollection = false; @@ -519,8 +539,6 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } } - updateRecentAppsToFit(); - if (mActivityContext.getDeviceProfile().isQsbInline) { addView(mQsb, mIsRtl ? getChildCount() : 0); // Always set QSB to invisible after re-adding. @@ -528,45 +546,6 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } } - /** - * Updates the recent apps portion of the taskbar by: - * - Removing overflow affordance if overflow is not needed. - * - Removing any recent apps that do not fit. - */ - private void updateRecentAppsToFit() { - if (!Flags.taskbarOverflow()) { - return; - } - int indexOfFirstRecentApp = -1; - int size = getChildCount(); - boolean removeOverflowView = true; - - for (int i = 0; i < size; ++i) { - if (getChildAt(i).getTag() instanceof GroupTask) { - indexOfFirstRecentApp = i; - removeOverflowView = false; - break; - } - } - - if (indexOfFirstRecentApp != -1) { - // We pre-maturely added the overflow icon, so we can take it out of the count. - int numRecentAppsToRemove = Math.max(0, getChildCount() - mMaxNumIcons + 1); - if (numRecentAppsToRemove <= 1) { - // We can fit all of the recent apps if we remove the overflow icon. - removeOverflowView = true; - } else { - for (int i = 0; i < numRecentAppsToRemove; ++i) { - removeAndRecycle(getChildAt(indexOfFirstRecentApp)); - } - } - } - - if (removeOverflowView) { - removeView(mTaskbarOverflowView); - } - } - /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */ public void applyGroupTaskToBubbleTextView(BubbleTextView btv, GroupTask groupTask) { // TODO(b/343289567): support app pairs. diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java index 176be1ce5b..8bc1e12395 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java @@ -19,6 +19,7 @@ package com.android.launcher3.taskbar; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP; +import android.content.Context; import android.view.InputDevice; import android.view.MotionEvent; import android.view.View; @@ -64,7 +65,8 @@ public class TaskbarViewCallbacks { mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS); } - public boolean isAllAppsButtonHapticFeedbackEnabled() { + /** @return true if haptic feedback should occur when long pressing the all apps button. */ + public boolean isAllAppsButtonHapticFeedbackEnabled(Context context) { return false; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt index ba0f5a01b0..704d6cf6c1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt @@ -16,10 +16,14 @@ package com.android.launcher3.taskbar +import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_META import android.content.Context import com.android.launcher3.R +import com.android.launcher3.logging.StatsLogManager import com.android.launcher3.util.ResourceBasedOverride import com.android.launcher3.util.ResourceBasedOverride.Overrides +import com.android.quickstep.TopTaskTracker +import com.android.quickstep.util.ContextualSearchInvoker /** Creates [TaskbarViewCallbacks] instances. */ open class TaskbarViewCallbacksFactory : ResourceBasedOverride { @@ -28,7 +32,35 @@ open class TaskbarViewCallbacksFactory : ResourceBasedOverride { activity: TaskbarActivityContext, controllers: TaskbarControllers, taskbarView: TaskbarView, - ): TaskbarViewCallbacks = TaskbarViewCallbacks(activity, controllers, taskbarView) + ): TaskbarViewCallbacks { + return object : TaskbarViewCallbacks(activity, controllers, taskbarView) { + override fun triggerAllAppsButtonLongClick() { + super.triggerAllAppsButtonLongClick() + + val contextualSearchInvoked = + ContextualSearchInvoker.newInstance(activity).show(ENTRYPOINT_LONG_PRESS_META) + if (contextualSearchInvoked) { + val runningPackage = + TopTaskTracker.INSTANCE[activity].getCachedTopTask( + /* filterOnlyVisibleRecents */ true + ) + .getPackageName() + activity.statsLogManager + .logger() + .withPackageName(runningPackage) + .log(StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_META) + } + } + + override fun isAllAppsButtonHapticFeedbackEnabled(context: Context): Boolean { + return longPressAllAppsToStartContextualSearch(context) + } + } + } + + open fun longPressAllAppsToStartContextualSearch(context: Context): Boolean = + ContextualSearchInvoker.newInstance(context) + .runContextualSearchInvocationChecksAndLogFailures() companion object { @JvmStatic diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index bc61c72212..c4d9e5028a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -234,7 +234,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar mTaskbarNavButtonTranslationY = controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY(); mTaskbarNavButtonTranslationYForInAppDisplay = controllers.navbarButtonsViewController - .getTaskbarNavButtonTranslationYForInAppDisplay(); + .getNavButtonTranslationYForInAppDisplay(); mActivity.addOnDeviceProfileChangeListener(mDeviceProfileChangeListener); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 69e1d4359d..63f101fd51 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -43,6 +43,7 @@ import com.android.launcher3.taskbar.TaskbarControllers; import com.android.launcher3.taskbar.TaskbarInsetsController; import com.android.launcher3.taskbar.TaskbarStashController; import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator; +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController; import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner; import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; import com.android.launcher3.util.MultiPropertyFactory; @@ -117,6 +118,8 @@ public class BubbleBarViewController { public boolean mOverflowAdded; private BubbleBarViewAnimator mBubbleBarViewAnimator; + private final FrameLayout mBubbleBarContainer; + private BubbleBarFlyoutController mBubbleBarFlyoutController; private final TimeSource mTimeSource = System::currentTimeMillis; @@ -127,6 +130,7 @@ public class BubbleBarViewController { FrameLayout bubbleBarContainer) { mActivity = activity; mBarView = barView; + mBubbleBarContainer = bubbleBarContainer; mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity); mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */); mIconSize = activity.getResources().getDimensionPixelSize( @@ -141,6 +145,8 @@ public class BubbleBarViewController { mBubbleDragController = bubbleControllers.bubbleDragController; mTaskbarStashController = controllers.taskbarStashController; mTaskbarInsetsController = controllers.taskbarInsetsController; + mBubbleBarFlyoutController = new BubbleBarFlyoutController( + mBubbleBarContainer, createFlyoutPositioner(), createFlyoutTopBoundaryListener()); mBubbleBarViewAnimator = new BubbleBarViewAnimator( mBarView, mBubbleStashController, mBubbleBarController::showExpandedView); mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider; @@ -266,6 +272,21 @@ public class BubbleBarViewController { }; } + private BubbleBarFlyoutController.TopBoundaryListener createFlyoutTopBoundaryListener() { + return new BubbleBarFlyoutController.TopBoundaryListener() { + @Override + public void extendTopBoundary(int space) { + int defaultSize = mActivity.getDefaultTaskbarWindowSize(); + mActivity.setTaskbarWindowSize(defaultSize + space); + } + + @Override + public void resetTopBoundary() { + mActivity.setTaskbarWindowSize(mActivity.getDefaultTaskbarWindowSize()); + } + }; + } + private void onBubbleClicked(BubbleView bubbleView) { bubbleView.markSeen(); BubbleBarItem bubble = bubbleView.getBubble(); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt index 49760ff113..c431deb27c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt @@ -21,20 +21,30 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.animation.ValueAnimator import com.android.launcher3.R +import com.android.systemui.util.doOnEnd +import com.android.systemui.util.doOnStart /** Creates and manages the visibility of the [BubbleBarFlyoutView]. */ -class BubbleBarFlyoutController( +class BubbleBarFlyoutController +@JvmOverloads +constructor( private val container: FrameLayout, private val positioner: BubbleBarFlyoutPositioner, + private val topBoundaryListener: TopBoundaryListener, + private val flyoutScheduler: FlyoutScheduler = HandlerScheduler(container), ) { + private companion object { + const val EXPAND_COLLAPSE_ANIMATION_DURATION_MS = 250L + } + private var flyout: BubbleBarFlyoutView? = null private val horizontalMargin = container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin) fun setUpFlyout(message: BubbleBarFlyoutMessage) { flyout?.let(container::removeView) - val flyout = BubbleBarFlyoutView(container.context, positioner) + val flyout = BubbleBarFlyoutView(container.context, positioner, flyoutScheduler) flyout.translationY = positioner.targetTy @@ -48,17 +58,43 @@ class BubbleBarFlyoutController( lp.marginEnd = horizontalMargin container.addView(flyout, lp) - val animator = ValueAnimator.ofFloat(0f, 1f) + val animator = + ValueAnimator.ofFloat(0f, 1f).setDuration(EXPAND_COLLAPSE_ANIMATION_DURATION_MS) animator.addUpdateListener { _ -> flyout.updateExpansionProgress(animator.animatedValue as Float) } + animator.doOnStart { + val flyoutTop = flyout.top + flyout.translationY + // If the top position of the flyout is negative, then it's bleeding over the + // top boundary of its parent view + if (flyoutTop < 0) topBoundaryListener.extendTopBoundary(space = -flyoutTop.toInt()) + } flyout.showFromCollapsed(message) { animator.start() } this.flyout = flyout } - fun hideFlyout() { + fun hideFlyout(endAction: () -> Unit) { val flyout = this.flyout ?: return - container.removeView(flyout) - this.flyout = null + val animator = + ValueAnimator.ofFloat(1f, 0f).setDuration(EXPAND_COLLAPSE_ANIMATION_DURATION_MS) + animator.addUpdateListener { _ -> + flyout.updateExpansionProgress(animator.animatedValue as Float) + } + animator.doOnEnd { + container.removeView(flyout) + this@BubbleBarFlyoutController.flyout = null + topBoundaryListener.resetTopBoundary() + endAction() + } + animator.start() + } + + /** Notifies when the top boundary of the flyout view changes. */ + interface TopBoundaryListener { + /** Requests to extend the top boundary of the parent to fully include the flyout. */ + fun extendTopBoundary(space: Int) + + /** Resets the top boundary of the parent. */ + fun resetTopBoundary() } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt index 2022a427db..c60fba2e21 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt @@ -36,14 +36,18 @@ import com.android.launcher3.R import com.android.launcher3.popup.RoundedArrowDrawable /** The flyout view used to notify the user of a new bubble notification. */ -class BubbleBarFlyoutView(context: Context, private val positioner: BubbleBarFlyoutPositioner) : - ConstraintLayout(context) { +class BubbleBarFlyoutView( + context: Context, + private val positioner: BubbleBarFlyoutPositioner, + scheduler: FlyoutScheduler? = null, +) : ConstraintLayout(context) { private companion object { // the minimum progress of the expansion animation before the content starts fading in. const val MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA = 0.75f } + private val scheduler: FlyoutScheduler = scheduler ?: HandlerScheduler(this) private val title: TextView by lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_title) } @@ -197,11 +201,10 @@ class BubbleBarFlyoutView(context: Context, private val positioner: BubbleBarFly // post the request to start the expand animation to the looper so the view can measure // itself - post(expandAnimation) + scheduler.runAfterLayout(expandAnimation) } private fun setData(flyoutMessage: BubbleBarFlyoutMessage) { - // the avatar is only displayed in group chat messages if (flyoutMessage.icon != null) { icon.visibility = VISIBLE icon.setImageDrawable(flyoutMessage.icon) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt new file mode 100644 index 0000000000..6f5d700b0f --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.bubbles.flyout + +import android.view.View + +/** Interface for scheduling jobs by flyout. */ +fun interface FlyoutScheduler { + /** Runs the given [block] after layout. */ + fun runAfterLayout(block: () -> Unit) +} + +/** A [FlyoutScheduler] that uses a Handler to schedule jobs. */ +class HandlerScheduler(val view: View) : FlyoutScheduler { + override fun runAfterLayout(block: () -> Unit) { + view.post(block) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt index e6c0b2f087..c5f8aa091d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt @@ -37,7 +37,7 @@ import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.launcher3.views.ActivityContext import com.android.launcher3.views.IconButtonView import com.android.quickstep.DeviceConfigWrapper -import com.android.quickstep.util.AssistStateManager +import com.android.quickstep.util.ContextualSearchStateManager /** Taskbar all apps button container for customizable taskbar. */ class TaskbarAllAppsButtonContainer @@ -79,17 +79,18 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 setOnClickListener(this::onAllAppsButtonClick) setOnLongClickListener(this::onAllAppsButtonLongClick) setOnTouchListener(this::onAllAppsButtonTouch) - isHapticFeedbackEnabled = taskbarViewCallbacks.isAllAppsButtonHapticFeedbackEnabled() + isHapticFeedbackEnabled = + taskbarViewCallbacks.isAllAppsButtonHapticFeedbackEnabled(mContext) allAppsTouchRunnable = Runnable { taskbarViewCallbacks.triggerAllAppsButtonLongClick() allAppsTouchTriggered = true } - val assistStateManager = AssistStateManager.INSTANCE[mContext] + val contextualSearchStateManager = ContextualSearchStateManager.INSTANCE[mContext] if ( DeviceConfigWrapper.get().customLpaaThresholds && - assistStateManager.lpnhDurationMillis.isPresent + contextualSearchStateManager.lpnhDurationMillis.isPresent ) { - allAppsButtonTouchDelayMs = assistStateManager.lpnhDurationMillis.get() + allAppsButtonTouchDelayMs = contextualSearchStateManager.lpnhDurationMillis.get() } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 39bf6ac814..ce4e9807e5 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -419,10 +419,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer, mDepthController.setActivityStarted(isStarted()); } - if ((changeBits & ACTIVITY_STATE_RESUMED) != 0) { - if (!FeatureFlags.enableHomeTransitionListener() && mTaskbarUIController != null) { - mTaskbarUIController.onLauncherVisibilityChanged(hasBeenResumed()); - } + if ((changeBits & ACTIVITY_STATE_RESUMED) != 0 && mTaskbarUIController != null) { + mTaskbarUIController.onLauncherPausedOrResumed(isPaused()); } super.onActivityFlagsChanged(changeBits); diff --git a/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt b/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt new file mode 100644 index 0000000000..46c4f365be --- /dev/null +++ b/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep + +import android.view.View +import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.R +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.popup.SystemShortcut +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskContainer +import com.android.window.flags.Flags +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus + +/** A menu item that allows the user to move the current app into external display. */ +class ExternalDisplaySystemShortcut( + container: RecentsViewContainer, + abstractFloatingViewHelper: AbstractFloatingViewHelper, + private val taskContainer: TaskContainer, +) : + SystemShortcut( + R.drawable.ic_external_display, + R.string.recent_task_option_external_display, + container, + taskContainer.itemInfo, + taskContainer.taskView, + abstractFloatingViewHelper, + ) { + override fun onClick(view: View) { + dismissTaskMenuView() + val recentsView = mTarget.getOverviewPanel>() + recentsView.moveTaskToExternalDisplay(taskContainer) { + mTarget.statsLogManager + .logger() + .withItemInfo(taskContainer.itemInfo) + .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP) + } + } + + companion object { + @JvmOverloads + /** + * Creates a factory for creating move task to external display system shortcuts in + * [com.android.quickstep.TaskOverlayFactory]. + */ + fun createFactory( + abstractFloatingViewHelper: AbstractFloatingViewHelper = AbstractFloatingViewHelper() + ): TaskShortcutFactory = + object : TaskShortcutFactory { + override fun getShortcuts( + container: RecentsViewContainer, + taskContainer: TaskContainer, + ): List? { + return if ( + DesktopModeStatus.canEnterDesktopMode(container.asContext()) && + Flags.moveToExternalDisplayShortcut() + ) + listOf( + ExternalDisplaySystemShortcut( + container, + abstractFloatingViewHelper, + taskContainer, + ) + ) + else null + } + + override fun showForGroupedTask() = true + } + } +} diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt index 461f963dc1..e23947b62d 100644 --- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt @@ -53,6 +53,7 @@ import com.android.systemui.shared.recents.model.ThumbnailData import com.android.systemui.shared.system.InteractionJankMonitorWrapper import java.io.PrintWriter import java.util.concurrent.ConcurrentLinkedDeque +import java.util.concurrent.Executor import kotlin.coroutines.resume import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -69,6 +70,7 @@ constructor( private val overviewComponentObserver: OverviewComponentObserver, private val taskAnimationManager: TaskAnimationManager, private val dispatcherProvider: DispatcherProvider = ProductionDispatchers, + private val uiExecutor: Executor = Executors.MAIN_EXECUTOR, ) { private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.default) @@ -85,7 +87,7 @@ constructor( get() = overviewComponentObserver.containerInterface private val visibleRecentsView: RecentsView<*, *>? - get() = containerInterface.getVisibleRecentsView>() + get() = containerInterface.getVisibleRecentsView() /** * Adds a command to be executed next, after all pending tasks are completed. Max commands that @@ -105,11 +107,7 @@ constructor( if (commandQueue.size == 1) { Log.d(TAG, "execute: $command - queue size: ${commandQueue.size}") - if (enableOverviewCommandHelperTimeout()) { - coroutineScope.launch(dispatcherProvider.main) { processNextCommand() } - } else { - Executors.MAIN_EXECUTOR.execute { processNextCommand() } - } + uiExecutor.execute { processNextCommand() } } else { Log.d(TAG, "not executed: $command - queue size: ${commandQueue.size}") } diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java index 51317743cd..de8be50541 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java @@ -19,6 +19,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.launcher3.MotionEventsUtils.isTrackpadScroll; import static com.android.launcher3.util.DisplayController.CHANGE_ALL; import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE; import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION; @@ -70,7 +71,7 @@ import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.SettingsCache; import com.android.quickstep.TopTaskTracker.CachedTaskInfo; import com.android.quickstep.util.ActiveGestureLog; -import com.android.quickstep.util.AssistStateManager; +import com.android.quickstep.util.ContextualSearchStateManager; import com.android.quickstep.util.GestureExclusionManager; import com.android.quickstep.util.GestureExclusionManager.ExclusionListener; import com.android.quickstep.util.NavBarPosition; @@ -101,7 +102,7 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, E private final DisplayController mDisplayController; private final GestureExclusionManager mExclusionManager; - private final AssistStateManager mAssistStateManager; + private final ContextualSearchStateManager mContextualSearchStateManager; private final RotationTouchHelper mRotationTouchHelper; private final TaskStackChangeListener mPipListener; @@ -152,7 +153,7 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, E mContext = context; mDisplayController = DisplayController.INSTANCE.get(context); mExclusionManager = exclusionManager; - mAssistStateManager = AssistStateManager.INSTANCE.get(context); + mContextualSearchStateManager = ContextualSearchStateManager.INSTANCE.get(context); mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false); mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(context); if (isInstanceForTouches) { @@ -563,6 +564,7 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, E return mAssistantAvailable && !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags) && mRotationTouchHelper.touchInAssistantRegion(ev) + && !isTrackpadScroll(ev) && !isLockToAppActive(); } @@ -617,8 +619,9 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, E : QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON; float touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); - if (mAssistStateManager.getLPNHCustomSlopMultiplier().isPresent()) { - float customSlopMultiplier = mAssistStateManager.getLPNHCustomSlopMultiplier().get(); + if (mContextualSearchStateManager.getLPNHCustomSlopMultiplier().isPresent()) { + float customSlopMultiplier = + mContextualSearchStateManager.getLPNHCustomSlopMultiplier().get(); return customSlopMultiplier * slopMultiplier * touchSlop; } else { return slopMultiplier * touchSlop; diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index 7a0d701724..c66813fe8b 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -64,7 +64,7 @@ import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.SafeCloseable; import com.android.quickstep.util.ActiveGestureProtoLogProxy; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -311,8 +311,8 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable { setBackToLauncherCallback(mBackToLauncherCallback, mBackToLauncherRunner); setUnfoldAnimationListener(mUnfoldAnimationListener); setDesktopTaskListener(mDesktopTaskListener); - setAssistantOverridesRequested( - AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes()); + setAssistantOverridesRequested(ContextualSearchInvoker.newInstance(mContext) + .getSysUiAssistOverrideInvocationTypes()); mStateChangeCallbacks.forEach(Runnable::run); if (mUnfoldTransitionProvider != null) { @@ -1493,6 +1493,17 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable { } } + /** Call shell to move a task with given `taskId` to external display. */ + public void moveToExternalDisplay(int taskId) { + if (mDesktopMode != null) { + try { + mDesktopMode.moveToExternalDisplay(taskId); + } catch (RemoteException e) { + Log.w(TAG, "Failed call moveToExternalDisplay", e); + } + } + } + // // Unfold transition // diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java index 8e45767f85..0dbdcb713a 100644 --- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java +++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java @@ -116,6 +116,7 @@ public class TaskOverlayFactory implements ResourceBasedOverride { TaskShortcutFactory.INSTALL, TaskShortcutFactory.FREE_FORM, DesktopSystemShortcut.Companion.createFactory(), + ExternalDisplaySystemShortcut.Companion.createFactory(), TaskShortcutFactory.WELLBEING, TaskShortcutFactory.SAVE_APP_PAIR, TaskShortcutFactory.SCREENSHOT, diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index f0943dc798..41a8a3150f 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -122,8 +122,8 @@ import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer; import com.android.quickstep.util.ActiveGestureLog; import com.android.quickstep.util.ActiveGestureLog.CompoundString; import com.android.quickstep.util.ActiveGestureProtoLogProxy; -import com.android.quickstep.util.AssistStateManager; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; +import com.android.quickstep.util.ContextualSearchStateManager; import com.android.quickstep.views.RecentsViewContainer; import com.android.systemui.shared.recents.IOverviewProxy; import com.android.systemui.shared.recents.ISystemUiProxy; @@ -297,7 +297,8 @@ public class TouchInteractionService extends Service { @Override public void onAssistantOverrideInvoked(int invocationType) { executeForTouchInteractionService(tis -> { - if (!AssistUtils.newInstance(tis).tryStartAssistOverride(invocationType)) { + if (!ContextualSearchInvoker.newInstance(tis) + .tryStartAssistOverride(invocationType)) { Log.w(TAG, "Failed to invoke Assist override"); } }); @@ -1640,8 +1641,8 @@ public class TouchInteractionService extends Service { } mTaskbarManager.dumpLogs("", pw); mDesktopVisibilityController.dumpLogs("", pw); - pw.println("AssistStateManager:"); - AssistStateManager.INSTANCE.get(this).dump("\t", pw); + pw.println("ContextualSearchStateManager:"); + ContextualSearchStateManager.INSTANCE.get(this).dump("\t", pw); SystemUiProxy.INSTANCE.get(this).dump(pw); DeviceConfigWrapper.get().dump(" ", pw); } diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java index 341c86892a..335161b24c 100644 --- a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java +++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java @@ -21,6 +21,7 @@ import com.android.launcher3.dagger.LauncherBaseAppComponent; import com.android.launcher3.model.WellbeingModel; import com.android.quickstep.logging.SettingsChangeLogger; import com.android.quickstep.util.AsyncClockEventDelegate; +import com.android.quickstep.util.ContextualSearchHapticManager; /** * Launcher Quickstep base component for Dagger injection. @@ -36,4 +37,6 @@ public interface QuickstepBaseAppComponent extends LauncherBaseAppComponent { WellbeingModel getWellbeingModel(); AsyncClockEventDelegate getAsyncClockEventDelegate(); + + ContextualSearchHapticManager getContextualSearchHapticManager(); } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java index 1d00e533f5..1a825a42e3 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java @@ -16,25 +16,67 @@ package com.android.quickstep.inputconsumers; +import static android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_NAV_HANDLE; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_SUCCESSFUL_NAV_HANDLE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OMNI_GET_LONG_PRESS_RUNNABLE; +import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_OMNI_RUNNABLE; + import android.content.Context; +import android.os.SystemClock; +import android.util.Log; +import android.view.ViewConfiguration; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.android.launcher3.LauncherApplication; import com.android.launcher3.R; +import com.android.launcher3.logging.InstanceId; +import com.android.launcher3.logging.InstanceIdSequence; +import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.util.ResourceBasedOverride; +import com.android.launcher3.util.VibratorWrapper; +import com.android.quickstep.DeviceConfigWrapper; import com.android.quickstep.NavHandle; +import com.android.quickstep.TopTaskTracker; +import com.android.quickstep.util.ContextualSearchHapticManager; +import com.android.quickstep.util.ContextualSearchInvoker; +import com.android.quickstep.util.ContextualSearchStateManager; /** * Class for extending nav handle long press behavior */ public class NavHandleLongPressHandler implements ResourceBasedOverride { + private static final String TAG = "NavHandleLongPressHandler"; + + protected final Context mContext; + protected final VibratorWrapper mVibratorWrapper; + protected final ContextualSearchHapticManager mContextualSearchHapticManager; + protected final ContextualSearchInvoker mContextualSearchInvoker; + protected final StatsLogManager mStatsLogManager; + private boolean mPendingInvocation; + + public NavHandleLongPressHandler(Context context) { + mContext = context; + mStatsLogManager = StatsLogManager.newInstance(context); + mVibratorWrapper = VibratorWrapper.INSTANCE.get(mContext); + mContextualSearchHapticManager = ((LauncherApplication) context.getApplicationContext()) + .getAppComponent().getContextualSearchHapticManager(); + mContextualSearchInvoker = ContextualSearchInvoker.newInstance(mContext); + } + /** Creates NavHandleLongPressHandler as specified by overrides */ public static NavHandleLongPressHandler newInstance(Context context) { return Overrides.getObject(NavHandleLongPressHandler.class, context, R.string.nav_handle_long_press_handler_class); } + protected boolean isContextualSearchEntrypointEnabled(NavHandle navHandle) { + return DeviceConfigWrapper.get().getEnableLongPressNavHandle(); + } + /** * Called when nav handle is long pressed to get the Runnable that should be executed by the * caller to invoke long press behavior. If null is returned that means long press couldn't be @@ -46,8 +88,48 @@ public class NavHandleLongPressHandler implements ResourceBasedOverride { * * @param navHandle to handle this long press */ - public @Nullable Runnable getLongPressRunnable(NavHandle navHandle) { - return null; + @Nullable + @VisibleForTesting + final Runnable getLongPressRunnable(NavHandle navHandle) { + if (!isContextualSearchEntrypointEnabled(navHandle)) { + Log.i(TAG, "Contextual Search invocation failed: entry point disabled"); + mVibratorWrapper.cancelVibrate(); + return null; + } + + if (!mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()) { + Log.i(TAG, "Contextual Search invocation failed: precondition not satisfied"); + mVibratorWrapper.cancelVibrate(); + return null; + } + + mPendingInvocation = true; + Log.i(TAG, "Contextual Search invocation: invocation runnable created"); + InstanceId instanceId = new InstanceIdSequence().newInstanceId(); + mStatsLogManager.logger().withInstanceId(instanceId).log( + LAUNCHER_OMNI_GET_LONG_PRESS_RUNNABLE); + long startTimeMillis = SystemClock.elapsedRealtime(); + return () -> { + mStatsLogManager.latencyLogger().withInstanceId(instanceId).withLatency( + SystemClock.elapsedRealtime() - startTimeMillis).log( + LAUNCHER_LATENCY_OMNI_RUNNABLE); + if (mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + ENTRYPOINT_LONG_PRESS_NAV_HANDLE)) { + Log.i(TAG, "Contextual Search invocation successful"); + + String runningPackage = TopTaskTracker.INSTANCE.get(mContext).getCachedTopTask( + /* filterOnlyVisibleRecents */ true).getPackageName(); + mStatsLogManager.logger().withPackageName(runningPackage) + .log(LAUNCHER_LAUNCH_ASSISTANT_SUCCESSFUL_NAV_HANDLE); + } else { + mVibratorWrapper.cancelVibrate(); + if (DeviceConfigWrapper.get().getAnimateLpnh() + && !DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) { + navHandle.animateNavBarLongPress( + /*isTouchDown*/false, /*shrink*/ false, /*durationMs*/160); + } + } + }; } /** @@ -55,7 +137,15 @@ public class NavHandleLongPressHandler implements ResourceBasedOverride { * * @param navHandle to handle the animation for this touch */ - public void onTouchStarted(NavHandle navHandle) {} + @VisibleForTesting + final void onTouchStarted(NavHandle navHandle) { + mPendingInvocation = false; + if (isContextualSearchEntrypointEnabled(navHandle) + && mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()) { + Log.i(TAG, "Contextual Search invocation: touch started"); + startNavBarAnimation(navHandle); + } + } /** * Called when nav handle gesture is finished by the user lifting their finger or the system @@ -64,5 +154,46 @@ public class NavHandleLongPressHandler implements ResourceBasedOverride { * @param navHandle to handle the animation for this touch * @param reason why the touch ended */ - public void onTouchFinished(NavHandle navHandle, String reason) {} + @VisibleForTesting + final void onTouchFinished(NavHandle navHandle, String reason) { + Log.i(TAG, "Contextual Search invocation: touch finished with reason: " + reason); + + if (!DeviceConfigWrapper.get().getShrinkNavHandleOnPress() || !mPendingInvocation) { + mVibratorWrapper.cancelVibrate(); + } + + if (DeviceConfigWrapper.get().getAnimateLpnh()) { + if (DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) { + navHandle.animateNavBarLongPress( + /*isTouchDown*/false, /*shrink*/ true, /*durationMs*/200); + } else { + navHandle.animateNavBarLongPress( + /*isTouchDown*/false, /*shrink*/ false, /*durationMs*/ 160); + } + } + } + + @VisibleForTesting + final void startNavBarAnimation(NavHandle navHandle) { + mContextualSearchHapticManager.vibrateForSearchHint(); + + if (DeviceConfigWrapper.get().getAnimateLpnh()) { + if (DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) { + navHandle.animateNavBarLongPress( + /*isTouchDown*/ true, /*shrink*/true, /*durationMs*/200); + } else { + long longPressTimeout; + ContextualSearchStateManager contextualSearchStateManager = + ContextualSearchStateManager.INSTANCE.get(mContext); + if (contextualSearchStateManager.getLPNHDurationMillis().isPresent()) { + longPressTimeout = + contextualSearchStateManager.getLPNHDurationMillis().get().intValue(); + } else { + longPressTimeout = ViewConfiguration.getLongPressTimeout(); + } + navHandle.animateNavBarLongPress( + /*isTouchDown*/ true, /*shrink*/ false, /*durationMs*/ longPressTimeout); + } + } + } } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java index f4d36953b5..f5bef05e14 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java @@ -38,7 +38,7 @@ import com.android.quickstep.InputConsumer; import com.android.quickstep.NavHandle; import com.android.quickstep.RecentsAnimationDeviceState; import com.android.quickstep.TopTaskTracker; -import com.android.quickstep.util.AssistStateManager; +import com.android.quickstep.util.ContextualSearchStateManager; import com.android.systemui.shared.system.InputMonitorCompat; /** @@ -75,9 +75,11 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { super(delegate, inputMonitor); mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x; mDeepPressEnabled = DeviceConfigWrapper.get().getEnableLpnhDeepPress(); - AssistStateManager assistStateManager = AssistStateManager.INSTANCE.get(context); - if (assistStateManager.getLPNHDurationMillis().isPresent()) { - mLongPressTimeout = assistStateManager.getLPNHDurationMillis().get().intValue(); + ContextualSearchStateManager contextualSearchStateManager = + ContextualSearchStateManager.INSTANCE.get(context); + if (contextualSearchStateManager.getLPNHDurationMillis().isPresent()) { + mLongPressTimeout = + contextualSearchStateManager.getLPNHDurationMillis().get().intValue(); } else { mLongPressTimeout = ViewConfiguration.getLongPressTimeout(); } diff --git a/quickstep/src/com/android/quickstep/util/AssistStateManager.java b/quickstep/src/com/android/quickstep/util/AssistStateManager.java deleted file mode 100644 index 7acb28da11..0000000000 --- a/quickstep/src/com/android/quickstep/util/AssistStateManager.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.util; - -import static com.android.launcher3.util.MainThreadInitializedObject.forOverride; - -import com.android.launcher3.R; -import com.android.launcher3.util.MainThreadInitializedObject; -import com.android.launcher3.util.ResourceBasedOverride; -import com.android.launcher3.util.SafeCloseable; - -import java.io.PrintWriter; -import java.util.Optional; - -/** Class to manage Assistant states. */ -public class AssistStateManager implements ResourceBasedOverride, SafeCloseable { - - public static final MainThreadInitializedObject INSTANCE = - forOverride(AssistStateManager.class, R.string.assist_state_manager_class); - - public AssistStateManager() {} - - /** Return {@code true} if the Settings toggle is enabled. */ - public boolean isSettingsAllEntrypointsEnabled() { - return false; - } - - /** Whether search supports showing on the lockscreen. */ - public boolean supportsShowWhenLocked() { - return false; - } - - /** Whether ContextualSearchService invocation path is available. */ - public boolean isContextualSearchServiceAvailable() { - return false; - } - - /** Get the Launcher overridden long press nav handle duration to trigger Assistant. */ - public Optional getLPNHDurationMillis() { - return Optional.empty(); - } - - /** - * Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant. - */ - public Optional getLPNHCustomSlopMultiplier() { - return Optional.empty(); - } - - /** Get the Launcher overridden long press home duration to trigger Assistant. */ - public Optional getLPHDurationMillis() { - return Optional.empty(); - } - - /** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */ - public Optional getLPHCustomSlopMultiplier() { - return Optional.empty(); - } - - /** Get the long press duration data source. */ - public int getDurationDataSource() { - return 0; - } - - /** Get the long press touch slop multiplier data source. */ - public int getSlopDataSource() { - return 0; - } - - /** Get the haptic bit overridden by AGSA. */ - public Optional getShouldPlayHapticOverride() { - return Optional.empty(); - } - - /** Dump states. */ - public void dump(String prefix, PrintWriter writer) {} - - @Override - public void close() {} -} diff --git a/quickstep/src/com/android/quickstep/util/AssistUtils.java b/quickstep/src/com/android/quickstep/util/AssistUtils.java deleted file mode 100644 index 11b6ea7d21..0000000000 --- a/quickstep/src/com/android/quickstep/util/AssistUtils.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.util; - -import android.content.Context; - -import com.android.launcher3.R; -import com.android.launcher3.util.ResourceBasedOverride; - -/** Utilities to work with Assistant functionality. */ -public class AssistUtils implements ResourceBasedOverride { - - public AssistUtils() {} - - /** Creates AssistUtils as specified by overrides */ - public static AssistUtils newInstance(Context context) { - return Overrides.getObject(AssistUtils.class, context, R.string.assist_utils_class); - } - - /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */ - public int[] getSysUiAssistOverrideInvocationTypes() { - return new int[0]; - } - - /** - * @return {@code true} if the override was handled, i.e. an assist surface was shown or the - * request should be ignored. {@code false} means the caller should start assist another way. - */ - public boolean tryStartAssistOverride(int invocationType) { - return false; - } -} diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt new file mode 100644 index 0000000000..8c246a5240 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.content.Context +import android.os.VibrationEffect +import android.os.VibrationEffect.Composition +import android.os.Vibrator +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.util.VibratorWrapper +import com.android.quickstep.DeviceConfigWrapper.Companion.get +import javax.inject.Inject +import kotlin.math.pow + +/** Manages haptics relating to Contextual Search invocations. */ +@LauncherAppSingleton +class ContextualSearchHapticManager +@Inject +internal constructor(@ApplicationContext private val context: Context) { + + private var searchEffect = createSearchEffect() + private var contextualSearchStateManager = ContextualSearchStateManager.INSTANCE[context] + + private fun createSearchEffect() = + if ( + context + .getSystemService(Vibrator::class.java)!! + .areAllPrimitivesSupported(Composition.PRIMITIVE_TICK) + ) { + VibrationEffect.startComposition() + .addPrimitive(Composition.PRIMITIVE_TICK, 1f) + .compose() + } else { + // fallback for devices without composition support + VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK) + } + + /** Indicates that search has been invoked. */ + fun vibrateForSearch() { + searchEffect.let { VibratorWrapper.INSTANCE[context].vibrate(it) } + } + + /** Indicates that search will be invoked if the current gesture is maintained. */ + fun vibrateForSearchHint() { + val navbarConfig = get() + // Whether we should play the hint (ramp up) haptic + val shouldVibrate: Boolean = + if ( + context + .getSystemService(Vibrator::class.java)!! + .areAllPrimitivesSupported(Composition.PRIMITIVE_LOW_TICK) + ) { + if (contextualSearchStateManager.shouldPlayHapticOverride.isPresent) { + contextualSearchStateManager.shouldPlayHapticOverride.get() + } else { + navbarConfig.enableSearchHapticHint + } + } else { + false + } + + if (shouldVibrate) { + val startScale = navbarConfig.lpnhHapticHintStartScalePercent / 100f + val endScale = navbarConfig.lpnhHapticHintEndScalePercent / 100f + val scaleExponent = navbarConfig.lpnhHapticHintScaleExponent + val iterations = navbarConfig.lpnhHapticHintIterations + val delayMs = navbarConfig.lpnhHapticHintDelay + val composition = VibrationEffect.startComposition() + for (i in 0 until iterations) { + val t = i / (iterations - 1f) + val scale = + ((1 - t) * startScale + t * endScale) + .toDouble() + .pow(scaleExponent.toDouble()) + .toFloat() + if (i == 0) { + // Adds a delay before the ramp starts + composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale, delayMs) + } else { + composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale) + } + } + VibratorWrapper.INSTANCE[context].vibrate(composition.compose()) + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt new file mode 100644 index 0000000000..dcb72aa4f3 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.app.contextualsearch.ContextualSearchManager +import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_HOME +import android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH +import android.content.Context +import android.util.Log +import com.android.internal.app.AssistUtils +import com.android.launcher3.LauncherApplication +import com.android.launcher3.R +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME +import com.android.launcher3.util.ResourceBasedOverride +import com.android.quickstep.DeviceConfigWrapper +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.TopTaskTracker +import com.android.systemui.shared.system.QuickStepContract + +/** Handles invocations and checks for Contextual Search. */ +open class ContextualSearchInvoker +internal constructor( + protected val context: Context, + private val contextualSearchStateManager: ContextualSearchStateManager, + private val topTaskTracker: TopTaskTracker, + private val systemUiProxy: SystemUiProxy, + protected val statsLogManager: StatsLogManager, + private val contextualSearchHapticManager: ContextualSearchHapticManager, + private val contextualSearchManager: ContextualSearchManager?, +) : ResourceBasedOverride { + constructor( + context: Context + ) : this( + context, + ContextualSearchStateManager.INSTANCE[context], + TopTaskTracker.INSTANCE[context], + SystemUiProxy.INSTANCE[context], + StatsLogManager.newInstance(context), + (context.applicationContext as LauncherApplication) + .appComponent + .contextualSearchHapticManager, + context.getSystemService(ContextualSearchManager::class.java), + ) + + /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */ + open fun getSysUiAssistOverrideInvocationTypes(): IntArray { + val overrideInvocationTypes = com.android.launcher3.util.IntArray() + if (context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { + overrideInvocationTypes.add(AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) + } + return overrideInvocationTypes.toArray() + } + + /** + * @return `true` if the override was handled, i.e. an assist surface was shown or the request + * should be ignored. `false` means the caller should start assist another way. + */ + fun tryStartAssistOverride(invocationType: Int): Boolean { + if (invocationType == AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) { + if (!context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { + // When Contextual Search is disabled, fall back to Assistant. + return false + } + + val success = show(ENTRYPOINT_LONG_PRESS_HOME) + if (success) { + val runningPackage = + TopTaskTracker.INSTANCE[context].getCachedTopTask( + /* filterOnlyVisibleRecents */ true + ) + .getPackageName() + statsLogManager + .logger() + .withPackageName(runningPackage) + .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME) + } + + // Regardless of success, do not fall back to other assistant. + return true + } + return false + } + + /** + * Invoke Contextual Search via ContextualSearchService if availability checks are successful + * + * @param entryPoint one of the ENTRY_POINT_* constants defined in this class + * @return true if invocation was successful, false otherwise + */ + fun show(entryPoint: Int): Boolean { + return if (!runContextualSearchInvocationChecksAndLogFailures()) false + else invokeContextualSearchUnchecked(entryPoint) + } + + /** + * Run availability checks and log errors to WW. If successful the caller is expected to call + * {@link invokeContextualSearchUnchecked} + * + * @return true if availability checks were successful, false otherwise. + */ + fun runContextualSearchInvocationChecksAndLogFailures(): Boolean { + if ( + contextualSearchManager == null || + !context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH) + ) { + Log.i(TAG, "Contextual Search invocation failed: no ContextualSearchManager") + statsLogManager.logger().log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR) + return false + } + if (!contextualSearchStateManager.isContextualSearchSettingEnabled) { + Log.i(TAG, "Contextual Search invocation failed: setting disabled") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED) + return false + } + if (isNotificationShadeShowing()) { + Log.i(TAG, "Contextual Search invocation failed: notification shade") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE) + return false + } + if (isKeyguardShowing()) { + Log.i(TAG, "Contextual Search invocation attempted: keyguard") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD) + if (!contextualSearchStateManager.isInvocationAllowedOnKeyguard) { + Log.i(TAG, "Contextual Search invocation failed: keyguard not allowed") + return false + } else if (!contextualSearchStateManager.supportsShowWhenLocked()) { + Log.i(TAG, "Contextual Search invocation failed: AGA doesn't support keyguard") + return false + } + } + if (isInSplitscreen()) { + Log.i(TAG, "Contextual Search invocation attempted: splitscreen") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN) + if (!contextualSearchStateManager.isInvocationAllowedInSplitscreen) { + Log.i(TAG, "Contextual Search invocation failed: splitscreen not allowed") + return false + } + } + if (!contextualSearchStateManager.isContextualSearchIntentAvailable) { + Log.i(TAG, "Contextual Search invocation failed: no matching CSS intent filter") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE) + return false + } + + return true + } + + /** + * Invoke Contextual Search via ContextualSearchService and do haptic + * + * @param entryPoint Entry point identifier, passed to ContextualSearchService. + * @return true if invocation was successful, false otherwise + */ + fun invokeContextualSearchUncheckedWithHaptic(entryPoint: Int): Boolean { + return invokeContextualSearchUnchecked(entryPoint, withHaptic = true) + } + + private fun invokeContextualSearchUnchecked( + entryPoint: Int, + withHaptic: Boolean = false, + ): Boolean { + if (withHaptic && DeviceConfigWrapper.get().enableSearchHapticCommit) { + contextualSearchHapticManager.vibrateForSearch() + } + if (contextualSearchManager == null) { + return false + } + contextualSearchManager.startContextualSearch(entryPoint) + return true + } + + private fun isInSplitscreen(): Boolean { + return topTaskTracker.getRunningSplitTaskIds().isNotEmpty() + } + + private fun isNotificationShadeShowing(): Boolean { + return systemUiProxy.lastSystemUiStateFlags and SHADE_EXPANDED_SYSUI_FLAGS != 0L + } + + private fun isKeyguardShowing(): Boolean { + return systemUiProxy.lastSystemUiStateFlags and KEYGUARD_SHOWING_SYSUI_FLAGS != 0L + } + + companion object { + private const val TAG = "ContextualSearchInvoker" + const val SHADE_EXPANDED_SYSUI_FLAGS = + QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED or + QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED + const val KEYGUARD_SHOWING_SYSUI_FLAGS = + (QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING or + QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING or + QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) + + @JvmStatic + fun newInstance(context: Context): ContextualSearchInvoker { + return ResourceBasedOverride.Overrides.getObject( + ContextualSearchInvoker::class.java, + context, + R.string.contextual_search_invoker_class, + ) + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java new file mode 100644 index 0000000000..142fc5843d --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import static android.app.contextualsearch.ContextualSearchManager.ACTION_LAUNCH_CONTEXTUAL_SEARCH; +import static android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_SYSTEM_ACTION; +import static android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import static com.android.launcher3.util.MainThreadInitializedObject.forOverride; +import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_SEARCH_SCREEN; + +import android.annotation.Nullable; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.content.Context; +import android.content.IIntentReceiver; +import android.content.IIntentSender; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Log; +import android.view.accessibility.AccessibilityManager; + +import androidx.annotation.CallSuper; +import androidx.annotation.VisibleForTesting; + +import com.android.launcher3.R; +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.util.EventLogArray; +import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.util.ResourceBasedOverride; +import com.android.launcher3.util.SafeCloseable; +import com.android.launcher3.util.SettingsCache; +import com.android.launcher3.util.SimpleBroadcastReceiver; +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TopTaskTracker; + +import java.io.PrintWriter; +import java.util.Optional; + +/** Long-lived class to manage Contextual Search states like the user setting and availability. */ +public class ContextualSearchStateManager implements ResourceBasedOverride, SafeCloseable { + + public static final MainThreadInitializedObject INSTANCE = + forOverride(ContextualSearchStateManager.class, + R.string.contextual_search_state_manager_class); + + private static final String TAG = "ContextualSearchStMgr"; + private static final int MAX_DEBUG_EVENT_SIZE = 20; + private static final Uri SEARCH_ALL_ENTRYPOINTS_ENABLED_URI = + Settings.Secure.getUriFor(Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED); + + private final Runnable mSysUiStateChangeListener = this::updateOverridesToSysUi; + private final SimpleBroadcastReceiver mContextualSearchPackageReceiver = + new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, (unused) -> requestUpdateProperties()); + private final SettingsCache.OnChangeListener mContextualSearchSettingChangedListener = + this::onContextualSearchSettingChanged; + protected final EventLogArray mEventLogArray = new EventLogArray(TAG, MAX_DEBUG_EVENT_SIZE); + + @Nullable private SettingsCache mSettingsCache; + // Cached value whether the ContextualSearch intent filter matched any enabled components. + private boolean mIsContextualSearchIntentAvailable; + private boolean mIsContextualSearchSettingEnabled; + + protected Context mContext; + protected String mContextualSearchPackage; + + public ContextualSearchStateManager() {} + + public ContextualSearchStateManager(Context context) { + mContext = context; + mContextualSearchPackage = mContext.getResources().getString( + com.android.internal.R.string.config_defaultContextualSearchPackageName); + + if (areAllContextualSearchFlagsDisabled() + || !context.getPackageManager().hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { + // If we had previously registered a SystemAction which is no longer valid, we need to + // unregister it here. + unregisterSearchScreenSystemAction(); + // Don't listen for stuff we aren't gonna use. + return; + } + + requestUpdateProperties(); + registerSearchScreenSystemAction(); + mContextualSearchPackageReceiver.registerPkgActions( + context, mContextualSearchPackage, Intent.ACTION_PACKAGE_ADDED, + Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED); + + mSettingsCache = SettingsCache.INSTANCE.get(context); + mSettingsCache.register(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI, + mContextualSearchSettingChangedListener); + onContextualSearchSettingChanged( + mSettingsCache.getValue(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI)); + SystemUiProxy.INSTANCE.get(mContext).addOnStateChangeListener(mSysUiStateChangeListener); + } + + /** Return {@code true} if the Settings toggle is enabled. */ + public final boolean isContextualSearchSettingEnabled() { + return mIsContextualSearchSettingEnabled; + } + + private void onContextualSearchSettingChanged(boolean isEnabled) { + mIsContextualSearchSettingEnabled = isEnabled; + } + + /** Whether search supports showing on the lockscreen. */ + protected boolean supportsShowWhenLocked() { + return false; + } + + /** Whether ContextualSearchService invocation path is available. */ + @VisibleForTesting + protected final boolean isContextualSearchIntentAvailable() { + return mIsContextualSearchIntentAvailable; + } + + /** Get the Launcher overridden long press nav handle duration to trigger Assistant. */ + public Optional getLPNHDurationMillis() { + return Optional.empty(); + } + + /** + * Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant. + */ + public Optional getLPNHCustomSlopMultiplier() { + return Optional.empty(); + } + + /** Get the Launcher overridden long press home duration to trigger Assistant. */ + public Optional getLPHDurationMillis() { + return Optional.empty(); + } + + /** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */ + public Optional getLPHCustomSlopMultiplier() { + return Optional.empty(); + } + + /** Get the long press duration data source. */ + public int getDurationDataSource() { + return 0; + } + + /** Get the long press touch slop multiplier data source. */ + public int getSlopDataSource() { + return 0; + } + + /** Get the haptic bit overridden by AGSA. */ + public Optional getShouldPlayHapticOverride() { + return Optional.empty(); + } + + protected boolean isInvocationAllowedOnKeyguard() { + return false; + } + + protected boolean isInvocationAllowedInSplitscreen() { + return true; + } + + @CallSuper + protected boolean areAllContextualSearchFlagsDisabled() { + return !DeviceConfigWrapper.get().getEnableLongPressNavHandle(); + } + + @CallSuper + protected void requestUpdateProperties() { + UI_HELPER_EXECUTOR.execute(() -> { + // Check that Contextual Search intent filters are enabled. + Intent csIntent = new Intent(ACTION_LAUNCH_CONTEXTUAL_SEARCH).setPackage( + mContextualSearchPackage); + mIsContextualSearchIntentAvailable = + !mContext.getPackageManager().queryIntentActivities(csIntent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE).isEmpty(); + + addEventLog("Updated isContextualSearchIntentAvailable", + mIsContextualSearchIntentAvailable); + }); + } + + protected final void updateOverridesToSysUi() { + // LPH commit haptic is always enabled + SystemUiProxy.INSTANCE.get(mContext).setOverrideHomeButtonLongPress( + getLPHDurationMillis().orElse(0L), getLPHCustomSlopMultiplier().orElse(0f), true); + Log.i(TAG, "Sent LPH override to sysui: " + getLPHDurationMillis().orElse(0L) + ";" + + getLPHCustomSlopMultiplier().orElse(0f)); + } + + private void registerSearchScreenSystemAction() { + PendingIntent searchScreenPendingIntent = new PendingIntent(new IIntentSender.Stub() { + @Override + public void send(int i, Intent intent, String s, IBinder iBinder, + IIntentReceiver iIntentReceiver, String s1, Bundle bundle) + throws RemoteException { + // Delayed slightly to minimize chance of capturing the System Actions dialog. + UI_HELPER_EXECUTOR.getHandler().postDelayed( + () -> { + boolean contextualSearchInvoked = + ContextualSearchInvoker.newInstance(mContext).show( + ENTRYPOINT_SYSTEM_ACTION); + if (contextualSearchInvoked) { + String runningPackage = + TopTaskTracker.INSTANCE.get(mContext).getCachedTopTask( + /* filterOnlyVisibleRecents */ + true).getPackageName(); + StatsLogManager.newInstance(mContext).logger() + .withPackageName(runningPackage) + .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION); + } + }, 200); + } + }); + + mContext.getSystemService(AccessibilityManager.class).registerSystemAction(new RemoteAction( + Icon.createWithResource(mContext, R.drawable.ic_allapps_search), + mContext.getString(R.string.search_gesture_feature_title), + mContext.getString(R.string.search_gesture_feature_title), + searchScreenPendingIntent), + SYSTEM_ACTION_ID_SEARCH_SCREEN); + } + + private void unregisterSearchScreenSystemAction() { + mContext.getSystemService(AccessibilityManager.class).unregisterSystemAction( + SYSTEM_ACTION_ID_SEARCH_SCREEN); + } + + /** Dump states. */ + public final void dump(String prefix, PrintWriter writer) { + synchronized (mEventLogArray) { + mEventLogArray.dump(prefix, writer); + } + } + + @Override + public void close() { + mContextualSearchPackageReceiver.unregisterReceiverSafely(mContext); + unregisterSearchScreenSystemAction(); + + if (mSettingsCache != null) { + mSettingsCache.unregister(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI, + mContextualSearchSettingChangedListener); + } + SystemUiProxy.INSTANCE.get(mContext).removeOnStateChangeListener(mSysUiStateChangeListener); + } + + protected final void addEventLog(String event) { + synchronized (mEventLogArray) { + mEventLogArray.addLog(event); + } + } + + protected final void addEventLog(String event, boolean extras) { + synchronized (mEventLogArray) { + mEventLogArray.addLog(event, extras); + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 7554c4496a..bccfa0c756 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -6673,6 +6673,26 @@ public abstract class RecentsView< successCallback.run(); } + /** + * Move the provided task into external display and invoke {@code successCallback} if succeeded. + */ + public void moveTaskToExternalDisplay(TaskContainer taskContainer, Runnable successCallback) { + if (!DesktopModeStatus.canEnterDesktopMode(mContext)) { + return; + } + switchToScreenshot(() -> finishRecentsAnimation(/* toRecents= */true, /* shouldPip= */false, + () -> moveTaskToDesktopInternal(taskContainer, successCallback))); + } + + private void moveTaskToDesktopInternal(TaskContainer taskContainer, Runnable successCallback) { + if (mDesktopRecentsTransitionController == null) { + return; + } + mDesktopRecentsTransitionController.moveToExternalDisplay(taskContainer.getTask().key.id); + successCallback.run(); + } + + // Logs when the orientation of Overview changes. We log both real and fake orientation changes. private void logOrientationChanged() { // Only log when Overview is showing. diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt index a57fb702d6..6e2f74acc7 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt @@ -16,10 +16,34 @@ package com.android.launcher3.taskbar +import android.content.Context import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.ConstantItem +import com.android.launcher3.LauncherPrefs +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty object TaskbarControllerTestUtil { inline fun runOnMainSync(crossinline runTest: () -> Unit) { getInstrumentation().runOnMainSync { runTest() } } + + /** Returns a property to read/write the value of a [ConstantItem]. */ + fun ConstantItem.asProperty(context: Context): ReadWriteProperty { + return TaskbarItemProperty(context, this) + } + + private class TaskbarItemProperty( + private val context: Context, + private val item: ConstantItem, + ) : ReadWriteProperty { + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return LauncherPrefs.get(context).get(item) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + runOnMainSync { LauncherPrefs.get(context).put(item, value) } + } + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt index af05d22391..e60717b6d8 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt @@ -18,14 +18,13 @@ package com.android.launcher3.taskbar import android.util.Log import com.android.launcher3.Utilities +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync import com.android.launcher3.taskbar.rules.TaskbarModeRule import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode -import com.android.launcher3.taskbar.rules.TaskbarPinningPreferenceRule -import com.android.launcher3.taskbar.rules.TaskbarPreferenceRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext @@ -47,19 +46,9 @@ class TaskbarEduTooltipControllerTest { @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() - @get:Rule(order = 1) - val tooltipStepPreferenceRule = - TaskbarPreferenceRule(context, OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.prefItem) + @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context) - @get:Rule(order = 2) - val searchEduPreferenceRule = - TaskbarPreferenceRule(context, OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN) - - @get:Rule(order = 3) val taskbarPinningPreferenceRule = TaskbarPinningPreferenceRule(context) - - @get:Rule(order = 4) val taskbarModeRule = TaskbarModeRule(context) - - @get:Rule(order = 5) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) @InjectController lateinit var taskbarEduTooltipController: TaskbarEduTooltipController @@ -68,6 +57,9 @@ class TaskbarEduTooltipControllerTest { private val wasInTestHarness = Utilities.isRunningInTestHarness() + private var tooltipStep by OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.prefItem.asProperty(context) + private var searchEduSeen by OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN.asProperty(context) + @Before fun setUp() { Log.e("Taskbar", "TaskbarEduTooltipControllerTest test started") @@ -85,7 +77,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(THREE_BUTTONS) fun testMaybeShowSwipeEdu_whenTaskbarIsInThreeButtonMode_doesNotShowSwipeEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE + tooltipStep = TOOLTIP_STEP_SWIPE assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) @@ -95,7 +87,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(TRANSIENT) fun testMaybeShowSwipeEdu_whenSwipeEduAlreadyShown_doesNotShowSwipeEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_FEATURES + tooltipStep = TOOLTIP_STEP_FEATURES assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) @@ -105,7 +97,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(TRANSIENT) fun testMaybeShowSwipeEdu_whenUserHasNotSeen_doesShowSwipeEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE + tooltipStep = TOOLTIP_STEP_SWIPE assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) @@ -115,7 +107,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(TRANSIENT) fun testMaybeShowFeaturesEdu_whenFeatureEduAlreadyShown_doesNotShowFeatureEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_NONE + tooltipStep = TOOLTIP_STEP_NONE assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) @@ -125,7 +117,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(TRANSIENT) fun testMaybeShowFeaturesEdu_whenUserHasNotSeen_doesShowFeatureEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_FEATURES + tooltipStep = TOOLTIP_STEP_FEATURES assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) @@ -135,7 +127,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(THREE_BUTTONS) fun testMaybeShowPinningEdu_whenTaskbarIsInThreeButtonMode_doesNotShowPinningEdu() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_PINNING + tooltipStep = TOOLTIP_STEP_PINNING assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) @@ -146,7 +138,7 @@ class TaskbarEduTooltipControllerTest { @TaskbarMode(TRANSIENT) fun testMaybeShowPinningEdu_whenUserHasNotSeen_doesShowPinningEdu() { // Test standalone pinning edu, where user has seen taskbar edu before, but not pinning edu. - tooltipStepPreferenceRule.value = TOOLTIP_STEP_PINNING + tooltipStep = TOOLTIP_STEP_PINNING assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) @@ -156,21 +148,21 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(TRANSIENT) fun testIsBeforeTooltipFeaturesStep_whenUserHasNotSeenFeatureEdu_shouldReturnTrue() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE + tooltipStep = TOOLTIP_STEP_SWIPE assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isTrue() } @Test @TaskbarMode(TRANSIENT) fun testIsBeforeTooltipFeaturesStep_whenUserHasSeenFeatureEdu_shouldReturnFalse() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_NONE + tooltipStep = TOOLTIP_STEP_NONE assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isFalse() } @Test @TaskbarMode(TRANSIENT) fun testHide_whenTooltipIsOpen_shouldCloseTooltip() { - tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE + tooltipStep = TOOLTIP_STEP_SWIPE assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } @@ -190,7 +182,7 @@ class TaskbarEduTooltipControllerTest { @Test @TaskbarMode(PINNED) fun testMaybeShowSearchEdu_whenTaskbarIsPinnedAndUserHasSeenSearchEdu_shouldNotShowSearchEdu() { - searchEduPreferenceRule.value = true + searchEduSeen = true assertThat(taskbarEduTooltipController.userHasSeenSearchEdu).isTrue() runOnMainSync { taskbarEduTooltipController.hide() } assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java index 02d62186b1..253d9215fe 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java @@ -39,7 +39,7 @@ import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TouchInteractionService; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; import com.android.systemui.contextualeducation.GestureType; import org.junit.Before; @@ -64,7 +64,7 @@ public class TaskbarNavButtonControllerTest { @Mock Handler mockHandler; @Mock - AssistUtils mockAssistUtils; + ContextualSearchInvoker mockContextualSearchInvoker; @Mock StatsLogManager mockStatsLogManager; @Mock @@ -109,7 +109,7 @@ public class TaskbarNavButtonControllerTest { mockSystemUiProxy, mockContextualEduStatsManager, mockHandler, - mockAssistUtils); + mockContextualSearchInvoker); } @Test @@ -166,40 +166,40 @@ public class TaskbarNavButtonControllerTest { @Test public void testLongPressHome_enabled_withoutOverride() { mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, times(1)).startAssistant(any()); } @Test public void testLongPressHome_enabled_withOverride() { mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } @Test public void testLongPressHome_disabled_withoutOverride() { mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } @Test public void testLongPressHome_disabled_withOverride() { mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt index de73ce7016..71f4ef4ee6 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt @@ -18,10 +18,13 @@ package com.android.launcher3.taskbar import android.animation.AnimatorTestRule import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING import com.android.launcher3.R import com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_STASHED import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE @@ -42,7 +45,6 @@ import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode -import com.android.launcher3.taskbar.rules.TaskbarPinningPreferenceRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode @@ -63,11 +65,11 @@ import org.junit.runner.RunWith @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @EmulatedDevices(["pixelTablet2023"]) class TaskbarStashControllerTest { - @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() - @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context) - @get:Rule(order = 2) val taskbarPinningPreferenceRule = TaskbarPinningPreferenceRule(context) - @get:Rule(order = 3) val animatorTestRule = AnimatorTestRule(this) - @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context) + @get:Rule(order = 4) val animatorTestRule = AnimatorTestRule(this) + @get:Rule(order = 5) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) @InjectController lateinit var stashController: TaskbarStashController @InjectController lateinit var viewController: TaskbarViewController @@ -121,10 +123,11 @@ class TaskbarStashControllerTest { @Test fun testRecreateAsTransient_timeoutStarted() { - taskbarPinningPreferenceRule.isPinned = true + var isPinned by TASKBAR_PINNING.asProperty(context) + isPinned = true activityContext.controllers.sharedState?.taskbarWasPinned = true - taskbarPinningPreferenceRule.isPinned = false + isPinned = false assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt index 516220a803..3c0d9c6a0e 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt @@ -21,6 +21,7 @@ import com.android.launcher3.R import com.android.launcher3.appprediction.AppsDividerView import com.android.launcher3.appprediction.AppsDividerView.DividerType import com.android.launcher3.appprediction.PredictionRowView +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty import com.android.launcher3.taskbar.TaskbarStashController import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IN_APP_AUTO import com.android.launcher3.taskbar.allapps.TaskbarAllAppsControllerTest.Companion.TEST_PREDICTED_APPS @@ -29,7 +30,6 @@ import com.android.launcher3.taskbar.rules.TaskbarModeRule import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode -import com.android.launcher3.taskbar.rules.TaskbarPreferenceRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext @@ -49,14 +49,12 @@ class TaskbarAllAppsViewControllerTest { @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context) - @get:Rule(order = 2) - val allAppsVisitedPreferenceRule = - TaskbarPreferenceRule(context, ALL_APPS_VISITED_COUNT.prefItem) - @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) @InjectController lateinit var overlayController: TaskbarOverlayController @InjectController lateinit var stashController: TaskbarStashController + private var allAppsVisitedCount by ALL_APPS_VISITED_COUNT.prefItem.asProperty(context) private val searchSessionController = TestUtil.getOnUiThread { TaskbarSearchSessionController.newInstance(context) } @@ -102,7 +100,7 @@ class TaskbarAllAppsViewControllerTest { @Test fun testShow_firstAllAppsVisit_hasAllAppsTextDivider() { - allAppsVisitedPreferenceRule.value = 0 + allAppsVisitedCount = 0 val viewController = createViewController() getInstrumentation().runOnMainSync { viewController.show(false) } @@ -120,7 +118,7 @@ class TaskbarAllAppsViewControllerTest { @Test fun testShow_maxAllAppsVisitedCount_hasLineDivider() { - allAppsVisitedPreferenceRule.value = ALL_APPS_VISITED_COUNT.maxCount + allAppsVisitedCount = ALL_APPS_VISITED_COUNT.maxCount val viewController = createViewController() getInstrumentation().runOnMainSync { viewController.show(false) } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt index fdafce082b..3dd7689a4e 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt @@ -22,12 +22,15 @@ import android.graphics.PointF import android.view.Gravity import android.widget.FrameLayout import android.widget.TextView +import androidx.core.animation.AnimatorTestRule import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.R import com.google.common.truth.Truth.assertThat import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -36,11 +39,15 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BubbleBarFlyoutControllerTest { + @get:Rule val animatorTestRule = AnimatorTestRule() + private lateinit var flyoutController: BubbleBarFlyoutController private lateinit var flyoutContainer: FrameLayout + private lateinit var topBoundaryListener: FakeTopBoundaryListener private val context = ApplicationProvider.getApplicationContext() private val flyoutMessage = BubbleBarFlyoutMessage(icon = null, "sender name", "message") private var onLeft = true + private var flyoutTy = 50f @Before fun setUp() { @@ -50,53 +57,126 @@ class BubbleBarFlyoutControllerTest { override val isOnLeft get() = onLeft - override val targetTy = 50f + override val targetTy + get() = flyoutTy + override val distanceToCollapsedPosition = PointF(100f, 200f) override val collapsedSize = 30f override val collapsedColor = Color.BLUE override val collapsedElevation = 1f override val distanceToRevealTriangle = 50f } - flyoutController = BubbleBarFlyoutController(flyoutContainer, positioner) + topBoundaryListener = FakeTopBoundaryListener() + val flyoutScheduler = FlyoutScheduler { block -> block.invoke() } + flyoutController = + BubbleBarFlyoutController( + flyoutContainer, + positioner, + topBoundaryListener, + flyoutScheduler, + ) } @Test fun flyoutPosition_left() { - flyoutController.setUpFlyout(flyoutMessage) - assertThat(flyoutContainer.childCount).isEqualTo(1) - val flyout = flyoutContainer.getChildAt(0) - val lp = flyout.layoutParams as FrameLayout.LayoutParams - assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.LEFT) - assertThat(flyout.translationY).isEqualTo(50f) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val lp = flyout.layoutParams as FrameLayout.LayoutParams + assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.LEFT) + assertThat(flyout.translationY).isEqualTo(50f) + } } @Test fun flyoutPosition_right() { onLeft = false - flyoutController.setUpFlyout(flyoutMessage) - assertThat(flyoutContainer.childCount).isEqualTo(1) - val flyout = flyoutContainer.getChildAt(0) - val lp = flyout.layoutParams as FrameLayout.LayoutParams - assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.RIGHT) - assertThat(flyout.translationY).isEqualTo(50f) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val lp = flyout.layoutParams as FrameLayout.LayoutParams + assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.RIGHT) + assertThat(flyout.translationY).isEqualTo(50f) + } } @Test fun flyoutMessage() { - flyoutController.setUpFlyout(flyoutMessage) - assertThat(flyoutContainer.childCount).isEqualTo(1) - val flyout = flyoutContainer.getChildAt(0) - val sender = flyout.findViewById(R.id.bubble_flyout_title) - assertThat(sender.text).isEqualTo("sender name") - val message = flyout.findViewById(R.id.bubble_flyout_text) - assertThat(message.text).isEqualTo("message") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val sender = flyout.findViewById(R.id.bubble_flyout_title) + assertThat(sender.text).isEqualTo("sender name") + val message = flyout.findViewById(R.id.bubble_flyout_text) + assertThat(message.text).isEqualTo("message") + } } @Test fun hideFlyout_removedFromContainer() { - flyoutController.setUpFlyout(flyoutMessage) - assertThat(flyoutContainer.childCount).isEqualTo(1) - flyoutController.hideFlyout() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + flyoutController.hideFlyout {} + animatorTestRule.advanceTimeBy(300) + } assertThat(flyoutContainer.childCount).isEqualTo(0) } + + @Test + fun showFlyout_extendsTopBoundary() { + // set negative translation for the flyout so that it will request to extend the top + // boundary + flyoutTy = -50f + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(300) + } + assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(50) + } + + @Test + fun showFlyout_withinBoundary() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(300) + } + assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(0) + } + + @Test + fun hideFlyout_resetsTopBoundary() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + flyoutController.hideFlyout {} + animatorTestRule.advanceTimeBy(300) + } + assertThat(topBoundaryListener.topBoundaryReset).isTrue() + } + + class FakeTopBoundaryListener : BubbleBarFlyoutController.TopBoundaryListener { + + var topBoundaryExtendedSpace = 0 + var topBoundaryReset = false + + override fun extendTopBoundary(space: Int) { + topBoundaryExtendedSpace = space + } + + override fun resetTopBoundary() { + topBoundaryReset = true + } + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt deleted file mode 100644 index d417790d84..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3.taskbar.rules - -import android.platform.test.flag.junit.FlagsParameterization -import android.platform.test.flag.junit.SetFlagsRule -import com.android.launcher3.Flags.FLAG_ENABLE_TASKBAR_PINNING -import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING -import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING_IN_DESKTOP_MODE -import com.android.launcher3.util.DisplayController -import org.junit.rules.RuleChain -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement - -/** - * Rule that allows modifying the Taskbar pinned preferences. - * - * The original preference values are restored on teardown. - * - * If this rule is being used with [TaskbarUnitTestRule], make sure this rule is applied first. - * - * This rule is overkill if a test does not need to change the mode during Taskbar's lifecycle. If - * the mode is static, use [TaskbarModeRule] instead, which forces the mode. A test can class can - * declare both this rule and [TaskbarModeRule] but using both for a test method is unsupported. - */ -class TaskbarPinningPreferenceRule(context: TaskbarWindowSandboxContext) : TestRule { - - private val setFlagsRule = - SetFlagsRule(FlagsParameterization(mapOf(FLAG_ENABLE_TASKBAR_PINNING to true))) - private val pinningRule = TaskbarPreferenceRule(context, TASKBAR_PINNING) - private val desktopPinningRule = TaskbarPreferenceRule(context, TASKBAR_PINNING_IN_DESKTOP_MODE) - private val ruleChain = - RuleChain.outerRule(setFlagsRule).around(pinningRule).around(desktopPinningRule) - - var isPinned by pinningRule::value - var isPinnedInDesktopMode by desktopPinningRule::value - - override fun apply(base: Statement, description: Description): Statement { - return object : Statement() { - override fun evaluate() { - DisplayController.enableTaskbarModePreferenceForTests(true) - try { - ruleChain.apply(base, description).evaluate() - } finally { - DisplayController.enableTaskbarModePreferenceForTests(false) - } - } - } - } -} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt deleted file mode 100644 index 977e7a5fa3..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3.taskbar.rules - -import com.android.launcher3.util.DisplayController -import com.android.launcher3.util.LauncherMultivalentJUnit -import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices -import com.android.launcher3.util.window.WindowManagerProxy -import com.google.android.apps.nexuslauncher.deviceemulator.TestWindowManagerProxy -import com.google.common.truth.Truth.assertThat -import org.junit.Rule -import org.junit.Test -import org.junit.runner.Description -import org.junit.runner.RunWith -import org.junit.runners.model.Statement - -@RunWith(LauncherMultivalentJUnit::class) -@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) -class TaskbarPinningPreferenceRuleTest { - @get:Rule val context = TaskbarWindowSandboxContext.create() - - private val preferenceRule = TaskbarPinningPreferenceRule(context) - - @Test - fun testEnablePinning_verifyDisplayController() { - onSetup { - preferenceRule.isPinned = true - preferenceRule.isPinnedInDesktopMode = false - assertThat(DisplayController.isPinnedTaskbar(context)).isTrue() - } - } - - @Test - fun testDisablePinning_verifyDisplayController() { - onSetup { - preferenceRule.isPinned = false - preferenceRule.isPinnedInDesktopMode = false - assertThat(DisplayController.isPinnedTaskbar(context)).isFalse() - } - } - - @Test - fun testEnableDesktopPinning_verifyDisplayController() { - context.putObject( - WindowManagerProxy.INSTANCE, - TestWindowManagerProxy(context).apply { isInDesktopMode = true }, - ) - - onSetup { - preferenceRule.isPinned = false - preferenceRule.isPinnedInDesktopMode = true - assertThat(DisplayController.isPinnedTaskbar(context)).isTrue() - } - } - - @Test - fun testDisableDesktopPinning_verifyDisplayController() { - context.putObject( - WindowManagerProxy.INSTANCE, - TestWindowManagerProxy(context).apply { isInDesktopMode = true }, - ) - - onSetup { - preferenceRule.isPinned = false - preferenceRule.isPinnedInDesktopMode = false - assertThat(DisplayController.isPinnedTaskbar(context)).isFalse() - } - } - - @Test - fun testTearDown_afterTogglingPinnedPreference_preferenceReset() { - val wasPinned = preferenceRule.isPinned - onSetup { preferenceRule.isPinned = !preferenceRule.isPinned } - assertThat(preferenceRule.isPinned).isEqualTo(wasPinned) - } - - @Test - fun testTearDown_afterTogglingDesktopPreference_preferenceReset() { - val wasPinnedInDesktopMode = preferenceRule.isPinnedInDesktopMode - onSetup { preferenceRule.isPinnedInDesktopMode = !preferenceRule.isPinnedInDesktopMode } - assertThat(preferenceRule.isPinnedInDesktopMode).isEqualTo(wasPinnedInDesktopMode) - } - - /** Executes [runTest] after the [preferenceRule] setup phase completes. */ - private fun onSetup(runTest: () -> Unit) { - preferenceRule - .apply( - object : Statement() { - override fun evaluate() = runTest() - }, - DESCRIPTION, - ) - .evaluate() - } - - private companion object { - private val DESCRIPTION = - Description.createSuiteDescription(TaskbarPinningPreferenceRule::class.java) - } -} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt deleted file mode 100644 index e42ca9ea30..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3.taskbar.rules - -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import com.android.launcher3.ConstantItem -import com.android.launcher3.LauncherPrefs -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement - -/** - * Rule for modifying a Taskbar preference. - * - * The original preference value is restored on teardown. - */ -class TaskbarPreferenceRule( - private val context: TaskbarWindowSandboxContext, - private val constantItem: ConstantItem, -) : TestRule { - - private val prefs: LauncherPrefs - get() = LauncherPrefs.get(context) - - var value: T - get() = prefs.get(constantItem) - set(value) = getInstrumentation().runOnMainSync { prefs.put(constantItem, value) } - - override fun apply(base: Statement, description: Description): Statement { - return object : Statement() { - override fun evaluate() { - val originalValue = value - try { - base.evaluate() - } finally { - value = originalValue - } - } - } - } -} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt deleted file mode 100644 index b7e6fa3f35..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3.taskbar.rules - -import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING -import com.android.launcher3.util.LauncherMultivalentJUnit -import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices -import com.google.common.truth.Truth.assertThat -import org.junit.Rule -import org.junit.Test -import org.junit.runner.Description -import org.junit.runner.RunWith -import org.junit.runners.model.Statement - -@RunWith(LauncherMultivalentJUnit::class) -@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) -class TaskbarPreferenceRuleTest { - - @get:Rule val context = TaskbarWindowSandboxContext.create() - private val preferenceRule = TaskbarPreferenceRule(context, TASKBAR_PINNING) - - @Test - fun testSetup_toggleBoolean_updatesPreferences() { - val originalValue = preferenceRule.value - onSetup { - preferenceRule.value = !preferenceRule.value - assertThat(preferenceRule.value).isNotEqualTo(originalValue) - } - } - - @Test - fun testTeardown_afterTogglingBoolean_preferenceReset() { - val originalValue = preferenceRule.value - onSetup { preferenceRule.value = !preferenceRule.value } - assertThat(preferenceRule.value).isEqualTo(originalValue) - } - - private fun onSetup(runTest: () -> Unit) { - preferenceRule - .apply( - object : Statement() { - override fun evaluate() = runTest() - }, - DESCRIPTION, - ) - .evaluate() - } - - private companion object { - private val DESCRIPTION = - Description.createSuiteDescription(TaskbarPreferenceRule::class.java) - } -} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt index 7728504e11..2d3bfd6556 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt @@ -22,6 +22,8 @@ import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.view.Display.DEFAULT_DISPLAY import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.FakeLauncherPrefs +import com.android.launcher3.LauncherPrefs import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox import com.android.launcher3.util.SandboxApplication import org.junit.rules.ExternalResource @@ -40,6 +42,10 @@ private constructor(base: SandboxApplication, val virtualDisplay: VirtualDisplay ObjectSandbox by base, TestRule by RuleChain.outerRule(virtualDisplayRule(virtualDisplay)).around(base) { + init { + putObject(LauncherPrefs.INSTANCE, FakeLauncherPrefs(this)) + } + companion object { private const val VIRTUAL_DISPLAY_NAME = "TaskbarSandboxDisplay" diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt index 0ae710f866..b0db737c94 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt @@ -41,7 +41,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.doAnswer import org.mockito.Mockito.spy -import org.mockito.Mockito.`when` import org.mockito.kotlin.any import org.mockito.kotlin.mock @@ -56,10 +55,12 @@ class OverviewCommandHelperTest { private val testScope = TestScope(dispatcher) private var pendingCallbacksWithDelays = mutableListOf() + private lateinit var pendingCommandsToExecute: MutableList @Suppress("UNCHECKED_CAST") @Before fun setup() { + pendingCommandsToExecute = mutableListOf() setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT) sut = @@ -68,7 +69,8 @@ class OverviewCommandHelperTest { touchInteractionService = mock(), overviewComponentObserver = mock(), taskAnimationManager = mock(), - dispatcherProvider = TestDispatcherProvider(dispatcher) + dispatcherProvider = TestDispatcherProvider(dispatcher), + uiExecutor = { runnable -> pendingCommandsToExecute += runnable }, ) ) @@ -94,12 +96,21 @@ class OverviewCommandHelperTest { pendingCallbacksWithDelays.add(delayInMillis) } + /** + * This function runs all the pending commands from the Executor for testing purposes. Replacing + * the uiExecutor allows the test to execute the command queue manually, making it possible to + * assert each state of the commands in the queue individually. + */ + private fun executePendingCommands() = pendingCommandsToExecute.forEach { it.run() } + @Test fun whenFirstCommandIsAdded_executeCommandImmediately() = testScope.runTest { // Add command to queue val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME)!! assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + executePendingCommands() + assertThat(commandInfo.status).isEqualTo(CommandStatus.PROCESSING) runCurrent() assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) } @@ -114,7 +125,7 @@ class OverviewCommandHelperTest { val commandInfo: CommandInfo = sut.addCommand(commandType)!! assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) - runCurrent() + executePendingCommands() assertThat(commandInfo.status).isEqualTo(CommandStatus.PROCESSING) advanceTimeBy(200L) @@ -135,12 +146,14 @@ class OverviewCommandHelperTest { val commandInfo2: CommandInfo = sut.addCommand(commandType2)!! assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) - runCurrent() + executePendingCommands() assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING) assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) advanceTimeBy(101L) assertThat(commandInfo1.status).isEqualTo(CommandStatus.COMPLETED) + + executePendingCommands() assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING) advanceTimeBy(101L) @@ -161,12 +174,14 @@ class OverviewCommandHelperTest { val commandInfo2: CommandInfo = sut.addCommand(commandType2)!! assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) - runCurrent() + executePendingCommands() assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING) assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) advanceTimeBy(QUEUE_TIMEOUT) assertThat(commandInfo1.status).isEqualTo(CommandStatus.CANCELED) + + executePendingCommands() assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING) advanceTimeBy(101) diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java new file mode 100644 index 0000000000..9018775316 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.inputconsumers; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.NavHandle; +import com.android.quickstep.util.TestExtensions; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NavHandleLongPressHandlerTest { + + private NavHandleLongPressHandler mLongPressHandler; + @Mock private NavHandle mNavHandle; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + mLongPressHandler = new NavHandleLongPressHandler(context); + } + + @Test + public void testStartNavBarAnimation_flagDisabled() { + try (AutoCloseable flag = overrideAnimateLPNHFlag(false)) { + mLongPressHandler.startNavBarAnimation(mNavHandle); + verify(mNavHandle, never()) + .animateNavBarLongPress(anyBoolean(), anyBoolean(), anyLong()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testStartNavBarAnimation_flagEnabled() { + try (AutoCloseable flag = overrideAnimateLPNHFlag(true)) { + mLongPressHandler.startNavBarAnimation(mNavHandle); + verify(mNavHandle).animateNavBarLongPress(anyBoolean(), anyBoolean(), anyLong()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private AutoCloseable overrideAnimateLPNHFlag(boolean value) { + return TestExtensions.overrideNavConfigFlag( + "ANIMATE_LPNH", value, () -> DeviceConfigWrapper.get().getAnimateLpnh()); + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java new file mode 100644 index 0000000000..543ffe61eb --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import static android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED; +import static com.android.quickstep.util.ContextualSearchInvoker.KEYGUARD_SHOWING_SYSUI_FLAGS; +import static com.android.quickstep.util.ContextualSearchInvoker.SHADE_EXPANDED_SYSUI_FLAGS; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.contextualsearch.ContextualSearchManager; +import android.content.Context; +import android.content.pm.PackageManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.logging.StatsLogManager; +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TopTaskTracker; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Robolectric unit tests for {@link ContextualSearchInvoker} + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ContextualSearchInvokerTest { + + private static final int CONTEXTUAL_SEARCH_ENTRY_POINT = 123; + + private @Mock PackageManager mMockPackageManager; + private @Mock ContextualSearchStateManager mMockStateManager; + private @Mock TopTaskTracker mMockTopTaskTracker; + private @Mock SystemUiProxy mMockSystemUiProxy; + private @Mock StatsLogManager mMockStatsLogManager; + private @Mock StatsLogManager.StatsLogger mMockStatsLogger; + private @Mock ContextualSearchHapticManager mMockContextualSearchHapticManager; + private @Mock ContextualSearchManager mMockContextualSearchManager; + private ContextualSearchInvoker mContextualSearchInvoker; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockPackageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)).thenReturn(true); + Context context = spy(getApplicationContext()); + doReturn(mMockPackageManager).when(context).getPackageManager(); + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(0L); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{}); + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(true); + when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(true); + when(mMockStatsLogManager.logger()).thenReturn(mMockStatsLogger); + + mContextualSearchInvoker = new ContextualSearchInvoker(context, mMockStateManager, + mMockTopTaskTracker, mMockSystemUiProxy, mMockStatsLogManager, + mMockContextualSearchHapticManager, mMockContextualSearchManager); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchFeatureIsNotAvailable() { + when(mMockPackageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)).thenReturn(false); + + assertFalse("Expected invocation to fail when feature is unavailable", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchIntentIsAvailable() { + assertTrue("Expected invocation checks to succeed", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verifyNoMoreInteractions(mMockStatsLogManager); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchIntentIsNotAvailable() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + assertFalse("Expected invocation to fail when feature is unavailable", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_settingDisabled() { + when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(false); + + assertFalse("Expected invocation checks to fail when setting is disabled", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_notificationShadeIsShowing() { + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(SHADE_EXPANDED_SYSUI_FLAGS); + + assertFalse("Expected invocation checks to fail when notification shade is showing", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_keyguardIsShowing() { + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn( + KEYGUARD_SHOWING_SYSUI_FLAGS); + + assertFalse("Expected invocation checks to fail when keyguard is showing", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_isInSplitScreen_disallowed() { + when(mMockStateManager.isInvocationAllowedInSplitscreen()).thenReturn(false); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{1, 2, 3}); + + assertFalse("Expected invocation checks to fail over split screen", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + // Attempt is logged regardless. + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_isInSplitScreen_allowed() { + when(mMockStateManager.isInvocationAllowedInSplitscreen()).thenReturn(true); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{1, 2, 3}); + + assertTrue("Expected invocation checks to succeed over split screen", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + // Attempt is logged regardless. + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN); + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsAvailable_commitHapticEnabled() { + try (AutoCloseable flag = overrideSearchHapticCommitFlag(true)) { + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockContextualSearchHapticManager).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsAvailable_commitHapticDisabled() { + try (AutoCloseable flag = overrideSearchHapticCommitFlag(false)) { + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockContextualSearchHapticManager, never()).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsNotAvailable_commitHapticEnabled() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + try (AutoCloseable flag = overrideSearchHapticCommitFlag(true)) { + // Still expect true since this method doesn't run the checks. + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + // Still vibrate based on the flag. + verify(mMockContextualSearchHapticManager).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsNotAvailable_commitHapticDisabled() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + try (AutoCloseable flag = overrideSearchHapticCommitFlag(false)) { + // Still expect true since this method doesn't run the checks. + assertTrue("Expected ContextualSearch invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + // Still don't vibrate based on the flag. + verify(mMockContextualSearchHapticManager, never()).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private AutoCloseable overrideSearchHapticCommitFlag(boolean value) { + return TestExtensions.overrideNavConfigFlag( + "ENABLE_SEARCH_HAPTIC_COMMIT", + value, + () -> DeviceConfigWrapper.get().getEnableSearchHapticCommit()); + } +} diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt new file mode 100644 index 0000000000..8968b9c55a --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep + +import android.content.ComponentName +import android.content.Intent +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.uioverrides.QuickstepLauncher +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.quickstep.TaskOverlayFactory.TaskOverlay +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.views.LauncherRecentsView +import com.android.quickstep.views.TaskContainer +import com.android.quickstep.views.TaskThumbnailViewDeprecated +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskViewIcon +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.android.window.flags.Flags +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +/** Test for ExternalDisplaySystemShortcut */ +class ExternalDisplaySystemShortcutTest { + + @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) + + private val launcher: QuickstepLauncher = mock() + private val statsLogManager: StatsLogManager = mock() + private val statsLogger: StatsLogManager.StatsLogger = mock() + private val recentsView: LauncherRecentsView = mock() + private val taskView: TaskView = mock() + private val workspaceItemInfo: WorkspaceItemInfo = mock() + private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock() + private val iconView: TaskViewIcon = mock() + private val transformingTouchDelegate: TransformingTouchDelegate = mock() + private val factory: TaskShortcutFactory = + ExternalDisplaySystemShortcut.createFactory(abstractFloatingViewHelper) + private val overlayFactory: TaskOverlayFactory = mock() + private val overlay: TaskOverlay<*> = mock() + + private lateinit var mockitoSession: StaticMockitoSession + + @Before + fun setUp() { + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.enforceDeviceRestrictions() } + ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + whenever(overlayFactory.createOverlay(any())).thenReturn(overlay) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @EnableFlags(Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT) + fun createExternalDisplayTaskShortcut_desktopModeDisabled() { + val task = createTask() + val taskContainer = createTaskContainer(task) + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + ) + fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported() { + ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + + val taskContainer = createTaskContainer(createTask()) + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + ) + fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported_overrideEnabled() { + ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.enforceDeviceRestrictions() } + + val taskContainer = spy(createTaskContainer(createTask())) + doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNotNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + ) + fun externalDisplaySystemShortcutClicked() { + val task = createTask() + val taskContainer = spy(createTaskContainer(task)) + + whenever(launcher.getOverviewPanel()).thenReturn(recentsView) + whenever(launcher.statsLogManager).thenReturn(statsLogManager) + whenever(statsLogManager.logger()).thenReturn(statsLogger) + whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger) + whenever(recentsView.moveTaskToExternalDisplay(any(), any())).thenAnswer { + val successCallback = it.getArgument(1) + successCallback.run() + } + doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).hasSize(1) + assertThat(shortcuts!!.first()).isInstanceOf(ExternalDisplaySystemShortcut::class.java) + + val externalDisplayShortcut = shortcuts.first() as ExternalDisplaySystemShortcut + + externalDisplayShortcut.onClick(taskView) + + val allTypesExceptRebindSafe = + AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv() + verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe) + verify(recentsView).moveTaskToExternalDisplay(eq(taskContainer), any()) + verify(statsLogger).withItemInfo(workspaceItemInfo) + verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP) + } + + private fun createTask(): Task = Task(TaskKey(1, 0, Intent(), ComponentName("", ""), 0, 2000)) + + private fun createTaskContainer(task: Task): TaskContainer { + val snapshotView = + if (enableRefactorTaskThumbnail()) mock() + else mock() + return TaskContainer( + taskView, + task, + snapshotView, + iconView, + transformingTouchDelegate, + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + digitalWellBeingToast = null, + showWindowsView = null, + overlayFactory, + ) + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java index e7e2f577ce..5ff2af705b 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java @@ -103,7 +103,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { @Test @NavigationModeSwitch @PortraitLandscape - @ScreenRecord // b/371615571 public void testWorkspaceSwitchToAllApps() { assertNotNull("switchToAllApps() returned null", mLauncher.getWorkspace().switchToAllApps()); diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java index 2e7526192d..3774ae3afe 100644 --- a/src/com/android/launcher3/BaseActivity.java +++ b/src/com/android/launcher3/BaseActivity.java @@ -306,6 +306,10 @@ public abstract class BaseActivity extends Activity implements ActivityContext { removeActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_DEFERRED_RESUMED); } + public boolean isPaused() { + return !hasBeenResumed() && (mActivityFlags & ACTIVITY_STATE_DEFERRED_RESUMED) == 0; + } + /** * Sets the activity to appear as resumed. */ diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java index 088277b5ca..c3508b77db 100644 --- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java +++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java @@ -22,6 +22,7 @@ import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.util.DaggerSingletonTracker; import com.android.launcher3.util.ScreenOnTracker; import com.android.launcher3.util.SettingsCache; +import com.android.launcher3.widget.custom.CustomWidgetManager; import dagger.BindsInstance; @@ -38,6 +39,7 @@ public interface LauncherBaseAppComponent { InstallSessionHelper getInstallSessionHelper(); ScreenOnTracker getScreenOnTracker(); SettingsCache getSettingsCache(); + CustomWidgetManager getCustomWidgetManager(); /** Builder for LauncherBaseAppComponent. */ interface Builder { diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java index fbd24d89f0..e5cd76ad00 100644 --- a/src/com/android/launcher3/logging/StatsLogManager.java +++ b/src/com/android/launcher3/logging/StatsLogManager.java @@ -221,6 +221,9 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User tapped on desktop icon on a task menu.") LAUNCHER_SYSTEM_SHORTCUT_DESKTOP_TAP(1706), + @UiEvent(doc = "Use tapped on external display icon on a task menu,") + LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP(1957), + @UiEvent(doc = "User tapped on pause app system shortcut.") LAUNCHER_SYSTEM_SHORTCUT_PAUSE_TAP(521), @@ -798,6 +801,44 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User long pressed on the taskbar IME switcher button") LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS(1798), + @UiEvent(doc = "Failed to launch assistant due to Google assistant not available") + LAUNCHER_LAUNCH_ASSISTANT_FAILED_NOT_AVAILABLE(1465), + + @UiEvent(doc = "Failed to launch assistant due to service error") + LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR(1466), + + @UiEvent(doc = "User launched assistant by long-pressing nav handle") + LAUNCHER_LAUNCH_ASSISTANT_SUCCESSFUL_NAV_HANDLE(1467), + + @UiEvent(doc = "Failed to launch due to Contextual Search not available") + LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE(1471), + + @UiEvent(doc = "Failed to launch due to Contextual Search setting disabled") + LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED(1632), + + @UiEvent(doc = "User launched Contextual Search by long-pressing home in 3-button mode") + LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME(1481), + + @UiEvent(doc = "User launched Contextual Search by using accessibility System Action") + LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION(1492), + + @UiEvent(doc = "User launched Contextual Search by long pressing the meta key") + LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_META(1606), + + @UiEvent(doc = "Contextual Search invocation was attempted over the notification shade") + LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE(1485), + + @UiEvent(doc = "The Contextual Search all entrypoints toggle value in Settings") + LAUNCHER_SETTINGS_OMNI_ALL_ENTRYPOINTS_TOGGLE_VALUE(1633), + + @UiEvent(doc = "Contextual Search invocation was attempted over the keyguard") + LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD(1501), + + @UiEvent(doc = "Contextual Search invocation was attempted while splitscreen is active") + LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN(1505), + + @UiEvent(doc = "User long press nav handle and a long press runnable was created.") + LAUNCHER_OMNI_GET_LONG_PRESS_RUNNABLE(1545), // ADD MORE ; @@ -828,6 +869,10 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "The duration of asynchronous loading workspace") LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC(1367), + + @UiEvent(doc = "Time passed between Contextual Search runnable creation and execution. This" + + " ensures that Recent animations have finished before Contextual Search starts.") + LAUNCHER_LATENCY_OMNI_RUNNABLE(1546), ; private final int mId; diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java index fba3936be9..b877d7ab61 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java @@ -98,7 +98,7 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo impleme } public void initSpans(Context context, InvariantDeviceProfile idp) { - mPM = context.getPackageManager(); + mPM = context.getApplicationContext().getPackageManager(); int minSpanX = 0; int minSpanY = 0; int maxSpanX = idp.numColumns; diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java index faa5d12875..0778172127 100644 --- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java +++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java @@ -33,7 +33,12 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.R; -import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.dagger.ApplicationContext; +import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.dagger.LauncherBaseAppComponent; +import com.android.launcher3.util.DaggerSingletonObject; +import com.android.launcher3.util.DaggerSingletonTracker; +import com.android.launcher3.util.ExecutorUtil; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.PluginManagerWrapper; import com.android.launcher3.util.SafeCloseable; @@ -50,13 +55,16 @@ import java.util.Map; import java.util.function.Consumer; import java.util.stream.Stream; +import javax.inject.Inject; + /** * CustomWidgetManager handles custom widgets implemented as a plugin. */ +@LauncherAppSingleton public class CustomWidgetManager implements PluginListener, SafeCloseable { - public static final MainThreadInitializedObject INSTANCE = - new MainThreadInitializedObject<>(CustomWidgetManager::new); + public static final DaggerSingletonObject INSTANCE = + new DaggerSingletonObject<>(LauncherBaseAppComponent::getCustomWidgetManager); private static final String TAG = "CustomWidgetManager"; private static final String PLUGIN_PKG = "android"; @@ -66,34 +74,44 @@ public class CustomWidgetManager implements PluginListener, private Consumer mWidgetRefreshCallback; private final @NonNull AppWidgetManager mAppWidgetManager; - private CustomWidgetManager(Context context) { - this(context, AppWidgetManager.getInstance(context)); + @Inject + CustomWidgetManager(@ApplicationContext Context context, DaggerSingletonTracker tracker) { + this(context, AppWidgetManager.getInstance(context), tracker); } @VisibleForTesting - CustomWidgetManager(Context context, @NonNull AppWidgetManager widgetManager) { + CustomWidgetManager(@ApplicationContext Context context, + @NonNull AppWidgetManager widgetManager, + DaggerSingletonTracker tracker) { mContext = context; mAppWidgetManager = widgetManager; mPlugins = new HashMap<>(); mCustomWidgets = new ArrayList<>(); - PluginManagerWrapper.INSTANCE.get(context) - .addPluginListener(this, CustomWidgetPlugin.class, true); - if (enableSmartspaceAsAWidget()) { - for (String s: context.getResources() - .getStringArray(R.array.custom_widget_providers)) { - try { - Class cls = Class.forName(s); - CustomWidgetPlugin plugin = (CustomWidgetPlugin) - cls.getDeclaredConstructor(Context.class).newInstance(context); - onPluginConnected(plugin, context); - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException - | ClassCastException | NoSuchMethodException - | InvocationTargetException e) { - Log.e(TAG, "Exception found when trying to add custom widgets: " + e); + + ExecutorUtil.executeSyncOnMainOrFail(() -> { + PluginManagerWrapper.INSTANCE.get(context) + .addPluginListener(this, CustomWidgetPlugin.class, true); + + if (enableSmartspaceAsAWidget()) { + for (String s: context.getResources() + .getStringArray(R.array.custom_widget_providers)) { + try { + Class cls = Class.forName(s); + CustomWidgetPlugin plugin = (CustomWidgetPlugin) + cls.getDeclaredConstructor(Context.class).newInstance(context); + onPluginConnected(plugin, context); + } catch (ClassNotFoundException | InstantiationException + | IllegalAccessException + | ClassCastException | NoSuchMethodException + | InvocationTargetException e) { + Log.e(TAG, "Exception found when trying to add custom widgets: " + e); + } } } - } + + tracker.addCloseable(this); + }); } @Override diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt index 058ac05fef..946bbc55ca 100644 --- a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt +++ b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt @@ -17,6 +17,7 @@ package com.android.launcher3 import android.content.Context +import com.android.launcher3.util.Executors.MAIN_EXECUTOR /** Emulates Launcher preferences for a test environment. */ class FakeLauncherPrefs(private val context: Context) : LauncherPrefs() { @@ -69,5 +70,8 @@ class FakeLauncherPrefs(private val context: Context) : LauncherPrefs() { override fun close() = Unit - private fun notifyChange(key: String) = listeners.forEach { it.onPrefChanged(key) } + private fun notifyChange(key: String) { + // Mimics SharedPreferencesImpl#notifyListeners main thread dispatching. + MAIN_EXECUTOR.execute { listeners.forEach { it.onPrefChanged(key) } } + } } diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt index b8e347c14b..2463c93d83 100644 --- a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt @@ -17,6 +17,7 @@ package com.android.launcher3 import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.launcher3.util.LauncherMultivalentJUnit import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -107,7 +108,7 @@ class FakeLauncherPrefsTest { fun testAddListener_changeItemInPrefs_callsListener() { var changedKey: String? = null launcherPrefs.addListener({ changedKey = it }, TEST_CONSTANT_ITEM) - launcherPrefs.put(TEST_CONSTANT_ITEM, true) + getInstrumentation().runOnMainSync { launcherPrefs.put(TEST_CONSTANT_ITEM, true) } assertThat(changedKey).isEqualTo(TEST_CONSTANT_ITEM.sharedPrefKey) } @@ -117,7 +118,7 @@ class FakeLauncherPrefsTest { launcherPrefs.put(TEST_CONSTANT_ITEM, true) launcherPrefs.addListener({ changedKey = it }, TEST_CONSTANT_ITEM) - launcherPrefs.remove(TEST_CONSTANT_ITEM) + getInstrumentation().runOnMainSync { launcherPrefs.remove(TEST_CONSTANT_ITEM) } assertThat(changedKey).isEqualTo(TEST_CONSTANT_ITEM.sharedPrefKey) } @@ -128,7 +129,7 @@ class FakeLauncherPrefsTest { launcherPrefs.addListener(listener, TEST_CONSTANT_ITEM) launcherPrefs.removeListener(listener) - launcherPrefs.put(TEST_CONSTANT_ITEM, true) + getInstrumentation().runOnMainSync { launcherPrefs.put(TEST_CONSTANT_ITEM, true) } assertThat(changedKey).isNull() } } diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt index 4b5710d5ac..82f56b8c2e 100644 --- a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt @@ -23,6 +23,7 @@ import android.platform.test.flag.junit.SetFlagsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.util.DaggerSingletonTracker import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext import com.android.launcher3.util.PluginManagerWrapper import com.android.launcher3.util.WidgetUtils @@ -57,12 +58,13 @@ class CustomWidgetManagerTest { @Mock private lateinit var pluginManager: PluginManagerWrapper @Mock private lateinit var mockAppWidgetManager: AppWidgetManager + @Mock private lateinit var tracker: DaggerSingletonTracker @Before fun setUp() { MockitoAnnotations.initMocks(this) context.putObject(PluginManagerWrapper.INSTANCE, pluginManager) - underTest = CustomWidgetManager(context, mockAppWidgetManager) + underTest = CustomWidgetManager(context, mockAppWidgetManager, tracker) } @After