Snap for 12502765 from 8915d3c3b8 to 25Q1-release
Change-Id: I4e714821abb3e9fd570e0e9d6d1fe7376d52f91b
This commit is contained in:
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
+42
-6
@@ -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)
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -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.
|
||||
|
||||
+24
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-25
@@ -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()
|
||||
|
||||
+11
-11
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+11
-8
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+5
-7
@@ -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) }
|
||||
|
||||
|
||||
+104
-24
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-65
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-114
@@ -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)
|
||||
}
|
||||
}
|
||||
-55
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-67
@@ -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)
|
||||
}
|
||||
}
|
||||
+6
@@ -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"
|
||||
|
||||
|
||||
+20
-5
@@ -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)
|
||||
|
||||
+79
@@ -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());
|
||||
}
|
||||
}
|
||||
+253
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user