Snap for 12502765 from 8915d3c3b8 to 25Q1-release

Change-Id: I4e714821abb3e9fd570e0e9d6d1fe7376d52f91b
This commit is contained in:
Android Build Coastguard Worker
2024-10-15 23:23:09 +00:00
60 changed files with 2009 additions and 677 deletions
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M320,840v-80L160,760q-33,0 -56.5,-23.5T80,680v-480q0,-33 23.5,-56.5T160,120h640q33,0 56.5,23.5T880,200v480q0,33 -23.5,56.5T800,760L640,760v80L320,840ZM160,680h640v-480L160,200v480ZM160,680v-480,480Z"
android:fillColor="#e8eaed"/>
</vector>
+2 -2
View File
@@ -37,8 +37,8 @@
<string name="taskbar_edu_tooltip_controller_class" translatable="false">com.android.launcher3.taskbar.TaskbarEduTooltipController</string>
<string name="contextual_edu_manager_class" translatable="false">com.android.quickstep.contextualeducation.SystemContextualEduStatsManager</string>
<string name="nav_handle_long_press_handler_class" translatable="false"></string>
<string name="assist_utils_class" translatable="false"></string>
<string name="assist_state_manager_class" translatable="false"></string>
<string name="contextual_search_invoker_class" translatable="false"></string>
<string name="contextual_search_state_manager_class" translatable="false"></string>
<string name="api_wrapper_class" translatable="false">com.android.launcher3.uioverrides.SystemApiWrapper</string>
<!-- The number of thumbnails and icons to keep in the cache. The thumbnail cache size also
+6
View File
@@ -28,6 +28,8 @@
<string name="recent_task_option_freeform">Freeform</string>
<!-- Title and content description for an option to enter desktop windowing mode for a given app -->
<string name="recent_task_option_desktop">Desktop</string>
<!-- Title and content description for an option to move app to external display. -->
<string name="recent_task_option_external_display">Move to external display</string>
<!-- Title and content description for Desktop tile in Recents screen that contains apps opened inside desktop windowing mode [CHAR LIMIT=NONE] -->
<string name="recent_task_desktop">Desktop</string>
@@ -360,4 +362,8 @@
<string name="bubble_bar_accessibility_announce_expand">expand <xliff:g id="bubble_description" example="some title from Messages">%1$s</xliff:g></string>
<!-- Accessibility announcement when the bubble bar collapses. [CHAR LIMIT=NONE]-->
<string name="bubble_bar_accessibility_announce_collapse">collapse <xliff:g id="bubble_description" example="some title from Messages">%1$s</xliff:g></string>
<!-- Name of Google's new feature to circle to search anything on your phone screen, without
switching apps. [CHAR_LIMIT=60] -->
<string name="search_gesture_feature_title">Circle to Search</string>
</resources>
@@ -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,
@@ -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();
}
@@ -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<AnimatedFloat> mTaskbarInAppDisplayProgressMultiProp =
new MultiPropertyFactory<>(mTaskbarInAppDisplayProgress,
AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max);
private final AnimatedFloat mLauncherPauseProgress = new AnimatedFloat(
this::launcherPauseProgressUpdate);
private final QuickstepLauncher mLauncher;
private final HomeVisibilityState mHomeState;
@@ -190,6 +194,33 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
placeholderDuration));
}
/**
* Called when Launcher Activity is paused/resumed.
* <p>
* To avoid UI clash between taskbar & bottom sheet, shift nav buttons down on launcher
* pause/resume at home.
* @param paused if launcher is currently paused.
*/
public void onLauncherPausedOrResumed(boolean paused) {
if (!FeatureFlags.enableHomeTransitionListener()) {
onLauncherVisibilityChanged(mLauncher.hasBeenResumed());
return;
}
// Animate navbar iff pause/resume from home, NOT to/from app (avoid overriding existing
// animations).
boolean launcherPauseOrResumeFromHome = mHomeState.isHomeVisible() && mControllers
.taskbarAutohideSuspendController.isSuspendedForTransientTaskbarInLauncher();
if (launcherPauseOrResumeFromHome) {
mLauncherPauseProgress.animateToValue(paused ? 1.0f : 0.0f).start();
}
}
private void launcherPauseProgressUpdate() {
onTaskbarInAppDisplayProgressUpdate(
mLauncherPauseProgress.value, LAUNCHER_PAUSE_PROGRESS_INDEX);
}
/**
* Should be called from onResume() and onPause(), and animates the Taskbar accordingly.
*/
@@ -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;
}
@@ -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="
@@ -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,
)
}
}
@@ -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();
@@ -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);
@@ -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.
*/
@@ -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.
@@ -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;
}
@@ -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
@@ -234,7 +234,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar
mTaskbarNavButtonTranslationY =
controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY();
mTaskbarNavButtonTranslationYForInAppDisplay = controllers.navbarButtonsViewController
.getTaskbarNavButtonTranslationYForInAppDisplay();
.getNavButtonTranslationYForInAppDisplay();
mActivity.addOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
@@ -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();
@@ -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()
}
}
@@ -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)
@@ -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)
}
}
@@ -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()
}
}
@@ -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);
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep
import android.view.View
import com.android.launcher3.AbstractFloatingViewHelper
import com.android.launcher3.R
import com.android.launcher3.logging.StatsLogManager.LauncherEvent
import com.android.launcher3.popup.SystemShortcut
import com.android.quickstep.views.RecentsView
import com.android.quickstep.views.RecentsViewContainer
import com.android.quickstep.views.TaskContainer
import com.android.window.flags.Flags
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
/** A menu item that allows the user to move the current app into external display. */
class ExternalDisplaySystemShortcut(
container: RecentsViewContainer,
abstractFloatingViewHelper: AbstractFloatingViewHelper,
private val taskContainer: TaskContainer,
) :
SystemShortcut<RecentsViewContainer>(
R.drawable.ic_external_display,
R.string.recent_task_option_external_display,
container,
taskContainer.itemInfo,
taskContainer.taskView,
abstractFloatingViewHelper,
) {
override fun onClick(view: View) {
dismissTaskMenuView()
val recentsView = mTarget.getOverviewPanel<RecentsView<*, *>>()
recentsView.moveTaskToExternalDisplay(taskContainer) {
mTarget.statsLogManager
.logger()
.withItemInfo(taskContainer.itemInfo)
.log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP)
}
}
companion object {
@JvmOverloads
/**
* Creates a factory for creating move task to external display system shortcuts in
* [com.android.quickstep.TaskOverlayFactory].
*/
fun createFactory(
abstractFloatingViewHelper: AbstractFloatingViewHelper = AbstractFloatingViewHelper()
): TaskShortcutFactory =
object : TaskShortcutFactory {
override fun getShortcuts(
container: RecentsViewContainer,
taskContainer: TaskContainer,
): List<ExternalDisplaySystemShortcut>? {
return if (
DesktopModeStatus.canEnterDesktopMode(container.asContext()) &&
Flags.moveToExternalDisplayShortcut()
)
listOf(
ExternalDisplaySystemShortcut(
container,
abstractFloatingViewHelper,
taskContainer,
)
)
else null
}
override fun showForGroupedTask() = true
}
}
}
@@ -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<RecentsView<*, *>>()
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}")
}
@@ -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;
@@ -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
//
@@ -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,
@@ -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);
}
@@ -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();
}
@@ -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);
}
}
}
}
@@ -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();
}
@@ -1,93 +0,0 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.util;
import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
import com.android.launcher3.R;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.ResourceBasedOverride;
import com.android.launcher3.util.SafeCloseable;
import java.io.PrintWriter;
import java.util.Optional;
/** Class to manage Assistant states. */
public class AssistStateManager implements ResourceBasedOverride, SafeCloseable {
public static final MainThreadInitializedObject<AssistStateManager> INSTANCE =
forOverride(AssistStateManager.class, R.string.assist_state_manager_class);
public AssistStateManager() {}
/** Return {@code true} if the Settings toggle is enabled. */
public boolean isSettingsAllEntrypointsEnabled() {
return false;
}
/** Whether search supports showing on the lockscreen. */
public boolean supportsShowWhenLocked() {
return false;
}
/** Whether ContextualSearchService invocation path is available. */
public boolean isContextualSearchServiceAvailable() {
return false;
}
/** Get the Launcher overridden long press nav handle duration to trigger Assistant. */
public Optional<Long> getLPNHDurationMillis() {
return Optional.empty();
}
/**
* Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant.
*/
public Optional<Float> getLPNHCustomSlopMultiplier() {
return Optional.empty();
}
/** Get the Launcher overridden long press home duration to trigger Assistant. */
public Optional<Long> getLPHDurationMillis() {
return Optional.empty();
}
/** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */
public Optional<Float> getLPHCustomSlopMultiplier() {
return Optional.empty();
}
/** Get the long press duration data source. */
public int getDurationDataSource() {
return 0;
}
/** Get the long press touch slop multiplier data source. */
public int getSlopDataSource() {
return 0;
}
/** Get the haptic bit overridden by AGSA. */
public Optional<Boolean> getShouldPlayHapticOverride() {
return Optional.empty();
}
/** Dump states. */
public void dump(String prefix, PrintWriter writer) {}
@Override
public void close() {}
}
@@ -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;
}
}
@@ -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())
}
}
}
@@ -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,
)
}
}
}
@@ -0,0 +1,281 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.util;
import static android.app.contextualsearch.ContextualSearchManager.ACTION_LAUNCH_CONTEXTUAL_SEARCH;
import static android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_SYSTEM_ACTION;
import static android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_SEARCH_SCREEN;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.Context;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.Log;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.CallSuper;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.R;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.util.EventLogArray;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.ResourceBasedOverride;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.SettingsCache;
import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.quickstep.DeviceConfigWrapper;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TopTaskTracker;
import java.io.PrintWriter;
import java.util.Optional;
/** Long-lived class to manage Contextual Search states like the user setting and availability. */
public class ContextualSearchStateManager implements ResourceBasedOverride, SafeCloseable {
public static final MainThreadInitializedObject<ContextualSearchStateManager> INSTANCE =
forOverride(ContextualSearchStateManager.class,
R.string.contextual_search_state_manager_class);
private static final String TAG = "ContextualSearchStMgr";
private static final int MAX_DEBUG_EVENT_SIZE = 20;
private static final Uri SEARCH_ALL_ENTRYPOINTS_ENABLED_URI =
Settings.Secure.getUriFor(Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED);
private final Runnable mSysUiStateChangeListener = this::updateOverridesToSysUi;
private final SimpleBroadcastReceiver mContextualSearchPackageReceiver =
new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, (unused) -> requestUpdateProperties());
private final SettingsCache.OnChangeListener mContextualSearchSettingChangedListener =
this::onContextualSearchSettingChanged;
protected final EventLogArray mEventLogArray = new EventLogArray(TAG, MAX_DEBUG_EVENT_SIZE);
@Nullable private SettingsCache mSettingsCache;
// Cached value whether the ContextualSearch intent filter matched any enabled components.
private boolean mIsContextualSearchIntentAvailable;
private boolean mIsContextualSearchSettingEnabled;
protected Context mContext;
protected String mContextualSearchPackage;
public ContextualSearchStateManager() {}
public ContextualSearchStateManager(Context context) {
mContext = context;
mContextualSearchPackage = mContext.getResources().getString(
com.android.internal.R.string.config_defaultContextualSearchPackageName);
if (areAllContextualSearchFlagsDisabled()
|| !context.getPackageManager().hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) {
// If we had previously registered a SystemAction which is no longer valid, we need to
// unregister it here.
unregisterSearchScreenSystemAction();
// Don't listen for stuff we aren't gonna use.
return;
}
requestUpdateProperties();
registerSearchScreenSystemAction();
mContextualSearchPackageReceiver.registerPkgActions(
context, mContextualSearchPackage, Intent.ACTION_PACKAGE_ADDED,
Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED);
mSettingsCache = SettingsCache.INSTANCE.get(context);
mSettingsCache.register(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
mContextualSearchSettingChangedListener);
onContextualSearchSettingChanged(
mSettingsCache.getValue(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI));
SystemUiProxy.INSTANCE.get(mContext).addOnStateChangeListener(mSysUiStateChangeListener);
}
/** Return {@code true} if the Settings toggle is enabled. */
public final boolean isContextualSearchSettingEnabled() {
return mIsContextualSearchSettingEnabled;
}
private void onContextualSearchSettingChanged(boolean isEnabled) {
mIsContextualSearchSettingEnabled = isEnabled;
}
/** Whether search supports showing on the lockscreen. */
protected boolean supportsShowWhenLocked() {
return false;
}
/** Whether ContextualSearchService invocation path is available. */
@VisibleForTesting
protected final boolean isContextualSearchIntentAvailable() {
return mIsContextualSearchIntentAvailable;
}
/** Get the Launcher overridden long press nav handle duration to trigger Assistant. */
public Optional<Long> getLPNHDurationMillis() {
return Optional.empty();
}
/**
* Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant.
*/
public Optional<Float> getLPNHCustomSlopMultiplier() {
return Optional.empty();
}
/** Get the Launcher overridden long press home duration to trigger Assistant. */
public Optional<Long> getLPHDurationMillis() {
return Optional.empty();
}
/** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */
public Optional<Float> getLPHCustomSlopMultiplier() {
return Optional.empty();
}
/** Get the long press duration data source. */
public int getDurationDataSource() {
return 0;
}
/** Get the long press touch slop multiplier data source. */
public int getSlopDataSource() {
return 0;
}
/** Get the haptic bit overridden by AGSA. */
public Optional<Boolean> getShouldPlayHapticOverride() {
return Optional.empty();
}
protected boolean isInvocationAllowedOnKeyguard() {
return false;
}
protected boolean isInvocationAllowedInSplitscreen() {
return true;
}
@CallSuper
protected boolean areAllContextualSearchFlagsDisabled() {
return !DeviceConfigWrapper.get().getEnableLongPressNavHandle();
}
@CallSuper
protected void requestUpdateProperties() {
UI_HELPER_EXECUTOR.execute(() -> {
// Check that Contextual Search intent filters are enabled.
Intent csIntent = new Intent(ACTION_LAUNCH_CONTEXTUAL_SEARCH).setPackage(
mContextualSearchPackage);
mIsContextualSearchIntentAvailable =
!mContext.getPackageManager().queryIntentActivities(csIntent,
PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE).isEmpty();
addEventLog("Updated isContextualSearchIntentAvailable",
mIsContextualSearchIntentAvailable);
});
}
protected final void updateOverridesToSysUi() {
// LPH commit haptic is always enabled
SystemUiProxy.INSTANCE.get(mContext).setOverrideHomeButtonLongPress(
getLPHDurationMillis().orElse(0L), getLPHCustomSlopMultiplier().orElse(0f), true);
Log.i(TAG, "Sent LPH override to sysui: " + getLPHDurationMillis().orElse(0L) + ";"
+ getLPHCustomSlopMultiplier().orElse(0f));
}
private void registerSearchScreenSystemAction() {
PendingIntent searchScreenPendingIntent = new PendingIntent(new IIntentSender.Stub() {
@Override
public void send(int i, Intent intent, String s, IBinder iBinder,
IIntentReceiver iIntentReceiver, String s1, Bundle bundle)
throws RemoteException {
// Delayed slightly to minimize chance of capturing the System Actions dialog.
UI_HELPER_EXECUTOR.getHandler().postDelayed(
() -> {
boolean contextualSearchInvoked =
ContextualSearchInvoker.newInstance(mContext).show(
ENTRYPOINT_SYSTEM_ACTION);
if (contextualSearchInvoked) {
String runningPackage =
TopTaskTracker.INSTANCE.get(mContext).getCachedTopTask(
/* filterOnlyVisibleRecents */
true).getPackageName();
StatsLogManager.newInstance(mContext).logger()
.withPackageName(runningPackage)
.log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION);
}
}, 200);
}
});
mContext.getSystemService(AccessibilityManager.class).registerSystemAction(new RemoteAction(
Icon.createWithResource(mContext, R.drawable.ic_allapps_search),
mContext.getString(R.string.search_gesture_feature_title),
mContext.getString(R.string.search_gesture_feature_title),
searchScreenPendingIntent),
SYSTEM_ACTION_ID_SEARCH_SCREEN);
}
private void unregisterSearchScreenSystemAction() {
mContext.getSystemService(AccessibilityManager.class).unregisterSystemAction(
SYSTEM_ACTION_ID_SEARCH_SCREEN);
}
/** Dump states. */
public final void dump(String prefix, PrintWriter writer) {
synchronized (mEventLogArray) {
mEventLogArray.dump(prefix, writer);
}
}
@Override
public void close() {
mContextualSearchPackageReceiver.unregisterReceiverSafely(mContext);
unregisterSearchScreenSystemAction();
if (mSettingsCache != null) {
mSettingsCache.unregister(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
mContextualSearchSettingChangedListener);
}
SystemUiProxy.INSTANCE.get(mContext).removeOnStateChangeListener(mSysUiStateChangeListener);
}
protected final void addEventLog(String event) {
synchronized (mEventLogArray) {
mEventLogArray.addLog(event);
}
}
protected final void addEventLog(String event, boolean extras) {
synchronized (mEventLogArray) {
mEventLogArray.addLog(event, extras);
}
}
}
@@ -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.
@@ -16,10 +16,34 @@
package com.android.launcher3.taskbar
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.ConstantItem
import com.android.launcher3.LauncherPrefs
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
object TaskbarControllerTestUtil {
inline fun runOnMainSync(crossinline runTest: () -> Unit) {
getInstrumentation().runOnMainSync { runTest() }
}
/** Returns a property to read/write the value of a [ConstantItem]. */
fun <T : Any> ConstantItem<T>.asProperty(context: Context): ReadWriteProperty<Any?, T> {
return TaskbarItemProperty(context, this)
}
private class TaskbarItemProperty<T : Any>(
private val context: Context,
private val item: ConstantItem<T>,
) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return LauncherPrefs.get(context).get(item)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
runOnMainSync { LauncherPrefs.get(context).put(item, value) }
}
}
}
@@ -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()
@@ -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());
}
@@ -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()
}
@@ -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) }
@@ -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<Context>()
private val flyoutMessage = BubbleBarFlyoutMessage(icon = null, "sender name", "message")
private var onLeft = true
private var flyoutTy = 50f
@Before
fun setUp() {
@@ -50,53 +57,126 @@ 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<TextView>(R.id.bubble_flyout_title)
assertThat(sender.text).isEqualTo("sender name")
val message = flyout.findViewById<TextView>(R.id.bubble_flyout_text)
assertThat(message.text).isEqualTo("message")
InstrumentationRegistry.getInstrumentation().runOnMainSync {
flyoutController.setUpFlyout(flyoutMessage)
assertThat(flyoutContainer.childCount).isEqualTo(1)
val flyout = flyoutContainer.getChildAt(0)
val sender = flyout.findViewById<TextView>(R.id.bubble_flyout_title)
assertThat(sender.text).isEqualTo("sender name")
val message = flyout.findViewById<TextView>(R.id.bubble_flyout_text)
assertThat(message.text).isEqualTo("message")
}
}
@Test
fun hideFlyout_removedFromContainer() {
flyoutController.setUpFlyout(flyoutMessage)
assertThat(flyoutContainer.childCount).isEqualTo(1)
flyoutController.hideFlyout()
InstrumentationRegistry.getInstrumentation().runOnMainSync {
flyoutController.setUpFlyout(flyoutMessage)
assertThat(flyoutContainer.childCount).isEqualTo(1)
flyoutController.hideFlyout {}
animatorTestRule.advanceTimeBy(300)
}
assertThat(flyoutContainer.childCount).isEqualTo(0)
}
@Test
fun showFlyout_extendsTopBoundary() {
// set negative translation for the flyout so that it will request to extend the top
// boundary
flyoutTy = -50f
InstrumentationRegistry.getInstrumentation().runOnMainSync {
flyoutController.setUpFlyout(flyoutMessage)
assertThat(flyoutContainer.childCount).isEqualTo(1)
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animatorTestRule.advanceTimeBy(300)
}
assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(50)
}
@Test
fun showFlyout_withinBoundary() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
flyoutController.setUpFlyout(flyoutMessage)
assertThat(flyoutContainer.childCount).isEqualTo(1)
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animatorTestRule.advanceTimeBy(300)
}
assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(0)
}
@Test
fun hideFlyout_resetsTopBoundary() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
flyoutController.setUpFlyout(flyoutMessage)
assertThat(flyoutContainer.childCount).isEqualTo(1)
flyoutController.hideFlyout {}
animatorTestRule.advanceTimeBy(300)
}
assertThat(topBoundaryListener.topBoundaryReset).isTrue()
}
class FakeTopBoundaryListener : BubbleBarFlyoutController.TopBoundaryListener {
var topBoundaryExtendedSpace = 0
var topBoundaryReset = false
override fun extendTopBoundary(space: Int) {
topBoundaryExtendedSpace = space
}
override fun resetTopBoundary() {
topBoundaryReset = true
}
}
}
@@ -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)
}
}
}
}
}
@@ -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)
}
}
@@ -1,55 +0,0 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.taskbar.rules
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.ConstantItem
import com.android.launcher3.LauncherPrefs
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* Rule for modifying a Taskbar preference.
*
* The original preference value is restored on teardown.
*/
class TaskbarPreferenceRule<T : Any>(
private val context: TaskbarWindowSandboxContext,
private val constantItem: ConstantItem<T>,
) : TestRule {
private val prefs: LauncherPrefs
get() = LauncherPrefs.get(context)
var value: T
get() = prefs.get(constantItem)
set(value) = getInstrumentation().runOnMainSync { prefs.put(constantItem, value) }
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
val originalValue = value
try {
base.evaluate()
} finally {
value = originalValue
}
}
}
}
}
@@ -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)
}
}
@@ -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"
@@ -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<Long>()
private lateinit var pendingCommandsToExecute: MutableList<Runnable>
@Suppress("UNCHECKED_CAST")
@Before
fun setup() {
pendingCommandsToExecute = mutableListOf()
setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT)
sut =
@@ -68,7 +69,8 @@ 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)
@@ -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());
}
}
@@ -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());
}
}
@@ -0,0 +1,193 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep
import android.content.ComponentName
import android.content.Intent
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.launcher3.AbstractFloatingView
import com.android.launcher3.AbstractFloatingViewHelper
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.logging.StatsLogManager
import com.android.launcher3.logging.StatsLogManager.LauncherEvent
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.uioverrides.QuickstepLauncher
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.quickstep.TaskOverlayFactory.TaskOverlay
import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.views.LauncherRecentsView
import com.android.quickstep.views.TaskContainer
import com.android.quickstep.views.TaskThumbnailViewDeprecated
import com.android.quickstep.views.TaskView
import com.android.quickstep.views.TaskViewIcon
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.Task.TaskKey
import com.android.window.flags.Flags
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
/** Test for ExternalDisplaySystemShortcut */
class ExternalDisplaySystemShortcutTest {
@get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT)
private val launcher: QuickstepLauncher = mock()
private val statsLogManager: StatsLogManager = mock()
private val statsLogger: StatsLogManager.StatsLogger = mock()
private val recentsView: LauncherRecentsView = mock()
private val taskView: TaskView = mock()
private val workspaceItemInfo: WorkspaceItemInfo = mock()
private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock()
private val iconView: TaskViewIcon = mock()
private val transformingTouchDelegate: TransformingTouchDelegate = mock()
private val factory: TaskShortcutFactory =
ExternalDisplaySystemShortcut.createFactory(abstractFloatingViewHelper)
private val overlayFactory: TaskOverlayFactory = mock()
private val overlay: TaskOverlay<*> = mock()
private lateinit var mockitoSession: StaticMockitoSession
@Before
fun setUp() {
mockitoSession =
mockitoSession()
.strictness(Strictness.LENIENT)
.spyStatic(DesktopModeStatus::class.java)
.startMocking()
ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.enforceDeviceRestrictions() }
ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
whenever(overlayFactory.createOverlay(any())).thenReturn(overlay)
}
@After
fun tearDown() {
mockitoSession.finishMocking()
}
@Test
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@EnableFlags(Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT)
fun createExternalDisplayTaskShortcut_desktopModeDisabled() {
val task = createTask()
val taskContainer = createTaskContainer(task)
val shortcuts = factory.getShortcuts(launcher, taskContainer)
assertThat(shortcuts).isNull()
}
@Test
@EnableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
)
fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported() {
ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
val taskContainer = createTaskContainer(createTask())
val shortcuts = factory.getShortcuts(launcher, taskContainer)
assertThat(shortcuts).isNull()
}
@Test
@EnableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
)
fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported_overrideEnabled() {
ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.enforceDeviceRestrictions() }
val taskContainer = spy(createTaskContainer(createTask()))
doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo
val shortcuts = factory.getShortcuts(launcher, taskContainer)
assertThat(shortcuts).isNotNull()
}
@Test
@EnableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
)
fun externalDisplaySystemShortcutClicked() {
val task = createTask()
val taskContainer = spy(createTaskContainer(task))
whenever(launcher.getOverviewPanel<LauncherRecentsView>()).thenReturn(recentsView)
whenever(launcher.statsLogManager).thenReturn(statsLogManager)
whenever(statsLogManager.logger()).thenReturn(statsLogger)
whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger)
whenever(recentsView.moveTaskToExternalDisplay(any(), any())).thenAnswer {
val successCallback = it.getArgument<Runnable>(1)
successCallback.run()
}
doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo
val shortcuts = factory.getShortcuts(launcher, taskContainer)
assertThat(shortcuts).hasSize(1)
assertThat(shortcuts!!.first()).isInstanceOf(ExternalDisplaySystemShortcut::class.java)
val externalDisplayShortcut = shortcuts.first() as ExternalDisplaySystemShortcut
externalDisplayShortcut.onClick(taskView)
val allTypesExceptRebindSafe =
AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv()
verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe)
verify(recentsView).moveTaskToExternalDisplay(eq(taskContainer), any())
verify(statsLogger).withItemInfo(workspaceItemInfo)
verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP)
}
private fun createTask(): Task = Task(TaskKey(1, 0, Intent(), ComponentName("", ""), 0, 2000))
private fun createTaskContainer(task: Task): TaskContainer {
val snapshotView =
if (enableRefactorTaskThumbnail()) mock<TaskThumbnailView>()
else mock<TaskThumbnailViewDeprecated>()
return TaskContainer(
taskView,
task,
snapshotView,
iconView,
transformingTouchDelegate,
SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
digitalWellBeingToast = null,
showWindowsView = null,
overlayFactory,
)
}
}
@@ -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());
@@ -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.
*/
@@ -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 {
@@ -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;
@@ -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;
@@ -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<CustomWidgetPlugin>, SafeCloseable {
public static final MainThreadInitializedObject<CustomWidgetManager> INSTANCE =
new MainThreadInitializedObject<>(CustomWidgetManager::new);
public static final DaggerSingletonObject<CustomWidgetManager> INSTANCE =
new DaggerSingletonObject<>(LauncherBaseAppComponent::getCustomWidgetManager);
private static final String TAG = "CustomWidgetManager";
private static final String PLUGIN_PKG = "android";
@@ -66,34 +74,44 @@ public class CustomWidgetManager implements PluginListener<CustomWidgetPlugin>,
private Consumer<PackageUserKey> mWidgetRefreshCallback;
private final @NonNull AppWidgetManager mAppWidgetManager;
private CustomWidgetManager(Context context) {
this(context, AppWidgetManager.getInstance(context));
@Inject
CustomWidgetManager(@ApplicationContext Context context, DaggerSingletonTracker tracker) {
this(context, AppWidgetManager.getInstance(context), tracker);
}
@VisibleForTesting
CustomWidgetManager(Context context, @NonNull AppWidgetManager widgetManager) {
CustomWidgetManager(@ApplicationContext Context context,
@NonNull AppWidgetManager widgetManager,
DaggerSingletonTracker tracker) {
mContext = context;
mAppWidgetManager = widgetManager;
mPlugins = new HashMap<>();
mCustomWidgets = new ArrayList<>();
PluginManagerWrapper.INSTANCE.get(context)
.addPluginListener(this, CustomWidgetPlugin.class, true);
if (enableSmartspaceAsAWidget()) {
for (String s: context.getResources()
.getStringArray(R.array.custom_widget_providers)) {
try {
Class<?> cls = Class.forName(s);
CustomWidgetPlugin plugin = (CustomWidgetPlugin)
cls.getDeclaredConstructor(Context.class).newInstance(context);
onPluginConnected(plugin, context);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException
| ClassCastException | NoSuchMethodException
| InvocationTargetException e) {
Log.e(TAG, "Exception found when trying to add custom widgets: " + e);
ExecutorUtil.executeSyncOnMainOrFail(() -> {
PluginManagerWrapper.INSTANCE.get(context)
.addPluginListener(this, CustomWidgetPlugin.class, true);
if (enableSmartspaceAsAWidget()) {
for (String s: context.getResources()
.getStringArray(R.array.custom_widget_providers)) {
try {
Class<?> cls = Class.forName(s);
CustomWidgetPlugin plugin = (CustomWidgetPlugin)
cls.getDeclaredConstructor(Context.class).newInstance(context);
onPluginConnected(plugin, context);
} catch (ClassNotFoundException | InstantiationException
| IllegalAccessException
| ClassCastException | NoSuchMethodException
| InvocationTargetException e) {
Log.e(TAG, "Exception found when trying to add custom widgets: " + e);
}
}
}
}
tracker.addCloseable(this);
});
}
@Override
@@ -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) } }
}
}
@@ -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()
}
}
@@ -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