Merge "[Memory Leak] Fix leak of NexusLauncehrActivity via AbsSwipeUpHandler#mRecentsAnimationStartCallbacks -> SurfaceTransactionApplier" into main
diff --git a/Android.bp b/Android.bp
index 1e1e0ad..a4a058f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -455,6 +455,9 @@
extra_check_modules: ["Launcher3LintChecker"],
baseline_filename: "lint-baseline.xml",
},
+ kotlincflags: [
+ "-Xjvm-default=all",
+ ],
}
// Library with all the dependencies for building quickstep
@@ -519,6 +522,9 @@
min_sdk_version: "current",
// TODO(b/319712088): re-enable use_resource_processor
use_resource_processor: false,
+ kotlincflags: [
+ "-Xjvm-default=all",
+ ],
}
// Library with all the source code and dependencies for building Quickstep
@@ -552,6 +558,9 @@
min_sdk_version: "current",
// TODO(b/319712088): re-enable use_resource_processor
use_resource_processor: false,
+ kotlincflags: [
+ "-Xjvm-default=all",
+ ],
}
// Build rule for Quickstep app.
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 80d2eac..fe57da1 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -136,7 +136,7 @@
TODO: Add proper permissions
-->
<provider
- android:name="com.android.launcher3.graphics.GridCustomizationsProvider"
+ android:name="com.android.launcher3.graphics.LauncherCustomizationProvider"
android:authorities="${applicationId}.grid_control"
android:exported="true" />
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index f379e22..5749c51 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -96,3 +96,17 @@
description: "Enable overview on connected displays."
bug: "363251602"
}
+
+flag {
+ name: "enable_overview_background_wallpaper_blur"
+ namespace: "launcher_overview"
+ description: "Enable wallpaper blur in overview."
+ bug: "369975912"
+}
+
+flag {
+ name: "enable_overview_desktop_tile_wallpaper_background"
+ namespace: "launcher_overview"
+ description: "Enable wallpaper background for desktop tasks in overview."
+ bug: "363257721"
+}
diff --git a/quickstep/res/layout/icon_app_chip_view.xml b/quickstep/res/layout/icon_app_chip_view.xml
index 00b5392..0972be1 100644
--- a/quickstep/res/layout/icon_app_chip_view.xml
+++ b/quickstep/res/layout/icon_app_chip_view.xml
@@ -51,7 +51,7 @@
<TextView
android:id="@+id/icon_text_collapsed"
android:layout_width="@dimen/task_thumbnail_icon_menu_text_collapsed_max_width"
- android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
+ android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:maxLines="1"
android:ellipsize="end"
@@ -62,7 +62,7 @@
<TextView
android:id="@+id/icon_text_expanded"
android:layout_width="@dimen/task_thumbnail_icon_menu_text_expanded_max_width"
- android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
+ android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:maxLines="1"
android:ellipsize="end"
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 5f2a63d..f8ca8d9 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -321,6 +321,7 @@
<item name="android:fontFamily">google-sans-text-medium</item>
<item name="android:textSize">@dimen/task_thumbnail_icon_menu_text_size</item>
<item name="android:textColor">@color/materialColorOnSurface</item>
+ <item name="android:includeFontPadding">false</item>
<item name="android:letterSpacing">0.025</item>
<item name="android:lineHeight">20sp</item>
</style>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index c748385..67feb6a 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -226,7 +226,6 @@
private static final int TASKBAR_TO_HOME_DURATION_FAST = 300;
private static final int TASKBAR_TO_HOME_DURATION_SLOW = 1000;
protected static final int CONTENT_SCALE_DURATION = 350;
- protected static final int CONTENT_SCRIM_DURATION = 350;
private static final int MAX_NUM_TASKS = 5;
@@ -244,7 +243,13 @@
private final StartingWindowListener mStartingWindowListener =
new StartingWindowListener(this);
- private ContentObserver mAnimationRemovalObserver = new ContentObserver(
+
+ // TODO(b/397690719): Investigate the memory leak from TaskStackChangeListeners#mImpl
+ // This is a temporary fix of memory leak b/397690719. We track registered
+ // {@link TaskRestartedDuringLaunchListener}, and remove them on activity destroy.
+ private final List<TaskRestartedDuringLaunchListener> mRegisteredTaskStackChangeListener =
+ new ArrayList<>();
+ private final ContentObserver mAnimationRemovalObserver = new ContentObserver(
ORDERED_BG_EXECUTOR.getHandler()) {
@Override
public void onChange(boolean selfChange) {
@@ -338,7 +343,14 @@
TaskRestartedDuringLaunchListener restartedListener =
new TaskRestartedDuringLaunchListener();
restartedListener.register(onEndCallback::executeAllAndDestroy);
- onEndCallback.add(restartedListener::unregister);
+ mRegisteredTaskStackChangeListener.add(restartedListener);
+ onEndCallback.add(new Runnable() {
+ @Override
+ public void run() {
+ restartedListener.unregister();
+ mRegisteredTaskStackChangeListener.remove(restartedListener);
+ }
+ });
RemoteAnimationRunnerCompat runner = createAppLaunchRunner(v, onEndCallback);
@@ -1210,6 +1222,12 @@
SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener(null);
ORDERED_BG_EXECUTOR.execute(() -> mLauncher.getContentResolver()
.unregisterContentObserver(mAnimationRemovalObserver));
+ if (BuildConfig.IS_STUDIO_BUILD && !mRegisteredTaskStackChangeListener.isEmpty()) {
+ throw new IllegalStateException("Failed to run onEndCallback created from"
+ + " getActivityLaunchOptions()");
+ }
+ mRegisteredTaskStackChangeListener.forEach(TaskRestartedDuringLaunchListener::unregister);
+ mRegisteredTaskStackChangeListener.clear();
}
/**
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 1cf7dda..f992913 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -322,13 +322,14 @@
stringCache.loadStrings(this);
bindStringCache(stringCache);
- bindWidgets(mModel.getWidgetsByPackageItem(), mModel.getDefaultWidgetsFilter());
+ bindWidgets(mModel.getWidgetsByPackageItemForPicker(),
+ mModel.getDefaultWidgetsFilter());
// Open sheet once widgets are available, so that it doesn't interrupt the open
// animation.
openWidgetsSheet();
if (mUiSurface != null) {
mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(),
- mUiSurface, mModel.getWidgetsByComponentKey());
+ mUiSurface, mModel.getWidgetsByComponentKeyForPicker());
mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets);
}
});
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
index adbcc75..688018b 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
@@ -21,6 +21,7 @@
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Rect
+import android.util.Log
import android.view.Choreographer
import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_CLOSE
@@ -63,7 +64,19 @@
fun createAnimators(info: TransitionInfo, finishCallback: (Animator) -> Unit): List<Animator> {
val launchChange = getLaunchChange(info)
- requireNotNull(launchChange) { "expected an app launch Change" }
+ requireNotNull(launchChange) {
+ val changesString =
+ info.changes.joinToString(", ") { change ->
+ "Change: mode=${change.mode}, " +
+ "taskId=${change.taskInfo?.id}, " +
+ "isFreeform=${change.taskInfo?.isFreeform}"
+ }
+ Log.e(
+ TAG,
+ "No launch change found: Transition type=${info.type}, changes=$changesString",
+ )
+ "expected an app launch Change"
+ }
val transaction = transactionSupplier.get()
@@ -173,10 +186,12 @@
onAnimFinish: (Animator) -> Unit,
): Animator {
return MinimizeAnimator.create(
- context.resources.displayMetrics,
+ context,
change,
transaction,
onAnimFinish,
+ interactionJankMonitor,
+ context.mainThreadHandler,
)
}
@@ -189,4 +204,8 @@
}
}
}
+
+ private companion object {
+ const val TAG = "DesktopAppLaunchAnimatorHelper"
+ }
}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index 578bba5..79072a6 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -19,6 +19,7 @@
import android.animation.Animator
import android.content.Context
import android.os.IBinder
+import android.util.Log
import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_FRONT
@@ -43,7 +44,7 @@
@JvmOverloads
constructor(
context: Context,
- launchType: AppLaunchType,
+ private val launchType: AppLaunchType,
@Cuj.CujType private val cujType: Int,
private val mainExecutor: Executor,
transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
@@ -66,6 +67,7 @@
transaction: Transaction,
transitionFinishedCallback: IRemoteTransitionFinishedCallback,
) {
+ Log.v(TAG, "startAnimation: launchType=$launchType, cujType=$cujType")
val safeTransitionFinishedCallback = RemoteRunnable {
transitionFinishedCallback.onTransitionFinished(/* wct= */ null, /* sct= */ null)
}
@@ -86,6 +88,7 @@
}
companion object {
+ const val TAG = "DesktopAppLaunchTransition"
/** Change modes that represent a task becoming visible / launching in Desktop mode. */
val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
index 40e1c10..9626a61 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -79,7 +79,7 @@
// Widgets (excluding shortcuts & already added widgets) that belong to apps eligible for
// being in predictions.
Map<ComponentKey, WidgetItem> allEligibleWidgets =
- dataModel.widgetsModel.getWidgetsByComponentKey()
+ dataModel.widgetsModel.getWidgetsByComponentKeyForPicker()
.entrySet()
.stream()
.filter(entry -> entry.getValue().widgetInfo != null
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
index 3773d02..6ee43ff 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.os.Debug
import android.util.Log
+import android.util.Slog
import android.util.SparseArray
import android.view.Display.DEFAULT_DISPLAY
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
@@ -61,8 +62,6 @@
* (Used only when multiple desks are enabled).
*
* @property displayId The ID of the display this object represents.
- * @property canCreateDesks true if it's possible to create new desks on the display represented
- * by this object.
* @property activeDeskId The ID of the active desk on the associated display (if any). It has a
* value of `INACTIVE_DESK_ID` (-1) if there are no active desks. Note that there can only be
* at most one active desk on each display.
@@ -70,11 +69,18 @@
*/
private data class DisplayDeskConfig(
val displayId: Int,
- var canCreateDesks: Boolean,
var activeDeskId: Int = INACTIVE_DESK_ID,
val deskIds: MutableSet<Int>,
)
+ /** True if it is possible to create new desks on current setup. */
+ var canCreateDesks: Boolean = false
+ private set(value) {
+ if (field == value) return
+ field = value
+ desktopVisibilityListeners.forEach { it.onCanCreateDesksChanged(field) }
+ }
+
/** Maps each display by its ID to its desks configuration. */
private val displaysDesksConfigsMap = SparseArray<DisplayDeskConfig>()
@@ -152,7 +158,8 @@
return areDesktopTasksVisible()
}
- val isInDesktopMode = displaysDesksConfigsMap[displayId].activeDeskId != INACTIVE_DESK_ID
+ val activeDeskId = getDisplayDeskConfig(displayId)?.activeDeskId ?: INACTIVE_DESK_ID
+ val isInDesktopMode = activeDeskId != INACTIVE_DESK_ID
if (DEBUG) {
Log.d(TAG, "isInDesktopMode: $isInDesktopMode")
}
@@ -391,7 +398,10 @@
}
}
- private fun onListenerConnected(displayDeskStates: Array<DisplayDeskState>) {
+ private fun onListenerConnected(
+ displayDeskStates: Array<DisplayDeskState>,
+ canCreateDesks: Boolean,
+ ) {
if (!DesktopModeStatus.enableMultipleDesktops(context)) {
return
}
@@ -402,25 +412,24 @@
displaysDesksConfigsMap[displayDeskState.displayId] =
DisplayDeskConfig(
displayId = displayDeskState.displayId,
- canCreateDesks = displayDeskState.canCreateDesk,
activeDeskId = displayDeskState.activeDeskId,
deskIds = displayDeskState.deskIds.toMutableSet(),
)
}
+
+ this.canCreateDesks = canCreateDesks
}
- private fun getDisplayDeskConfig(displayId: Int): DisplayDeskConfig {
- return checkNotNull(displaysDesksConfigsMap[displayId]) {
- "Expected non-null desk config for display: $displayId"
- }
- }
+ private fun getDisplayDeskConfig(displayId: Int) =
+ displaysDesksConfigsMap[displayId]
+ ?: null.also { Slog.e(TAG, "Expected non-null desk config for display: $displayId") }
- private fun onCanCreateDesksChanged(displayId: Int, canCreateDesks: Boolean) {
+ private fun onCanCreateDesksChanged(canCreateDesks: Boolean) {
if (!DesktopModeStatus.enableMultipleDesktops(context)) {
return
}
- getDisplayDeskConfig(displayId).canCreateDesks = canCreateDesks
+ this.canCreateDesks = canCreateDesks
}
private fun onDeskAdded(displayId: Int, deskId: Int) {
@@ -428,7 +437,7 @@
return
}
- getDisplayDeskConfig(displayId).also {
+ getDisplayDeskConfig(displayId)?.also {
check(it.deskIds.add(deskId)) {
"Found a duplicate desk Id: $deskId on display: $displayId"
}
@@ -440,7 +449,7 @@
return
}
- getDisplayDeskConfig(displayId).also {
+ getDisplayDeskConfig(displayId)?.also {
check(it.deskIds.remove(deskId)) {
"Removing non-existing desk Id: $deskId on display: $displayId"
}
@@ -457,7 +466,7 @@
val wasInDesktopMode = isInDesktopModeAndNotInOverview(displayId)
- getDisplayDeskConfig(displayId).also {
+ getDisplayDeskConfig(displayId)?.also {
check(oldActiveDesk == it.activeDeskId) {
"Mismatch between the Shell's oldActiveDesk: $oldActiveDesk, and Launcher's: ${it.activeDeskId}"
}
@@ -525,9 +534,12 @@
) : Stub() {
private val controller = WeakReference(controller)
- override fun onListenerConnected(displayDeskStates: Array<DisplayDeskState>) {
+ override fun onListenerConnected(
+ displayDeskStates: Array<DisplayDeskState>,
+ canCreateDesks: Boolean,
+ ) {
Executors.MAIN_EXECUTOR.execute {
- controller.get()?.onListenerConnected(displayDeskStates)
+ controller.get()?.onListenerConnected(displayDeskStates, canCreateDesks)
}
}
@@ -565,9 +577,9 @@
override fun onExitDesktopModeTransitionStarted(transitionDuration: Int) {}
- override fun onCanCreateDesksChanged(displayId: Int, canCreateDesks: Boolean) {
+ override fun onCanCreateDesksChanged(canCreateDesks: Boolean) {
Executors.MAIN_EXECUTOR.execute {
- controller.get()?.onCanCreateDesksChanged(displayId, canCreateDesks)
+ controller.get()?.onCanCreateDesksChanged(canCreateDesks)
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index d1e63f3..2e42e45 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -18,6 +18,7 @@
import static android.os.Trace.TRACE_TAG_APP;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
@@ -46,7 +47,6 @@
import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
-import static com.android.window.flags.Flags.enableStartLaunchTransitionFromTaskbarBugfix;
import static com.android.wm.shell.Flags.enableBubbleBar;
import static com.android.wm.shell.Flags.enableBubbleBarOnPhones;
import static com.android.wm.shell.Flags.enableTinyTaskbar;
@@ -876,7 +876,7 @@
@Override
public void onPopupVisibilityChanged(boolean isVisible) {
- setTaskbarWindowFocusable(isVisible);
+ setTaskbarWindowFocusable(isVisible /* focusable */, false /* imeFocusable */);
}
@Override
@@ -1235,17 +1235,29 @@
}
/**
- * Either adds or removes {@link WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} on the taskbar
- * window.
+ * Sets whether the taskbar window should be focusable and IME focusable. This won't be IME
+ * focusable unless it is also focusable.
+ *
+ * @param focusable whether it should be focusable.
+ * @param imeFocusable whether it should be IME focusable.
+ *
+ * @see WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE
+ * @see WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM
*/
- public void setTaskbarWindowFocusable(boolean focusable) {
+ public void setTaskbarWindowFocusable(boolean focusable, boolean imeFocusable) {
if (isPhoneMode()) {
return;
}
if (focusable) {
mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE;
+ if (imeFocusable) {
+ mWindowLayoutParams.flags &= ~FLAG_ALT_FOCUSABLE_IM;
+ } else {
+ mWindowLayoutParams.flags |= FLAG_ALT_FOCUSABLE_IM;
+ }
} else {
mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE;
+ mWindowLayoutParams.flags &= ~FLAG_ALT_FOCUSABLE_IM;
}
notifyUpdateLayoutParams();
}
@@ -1266,8 +1278,12 @@
}
/**
- * Either adds or removes {@link WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} on the taskbar
- * window. If we're now focusable, also move nav buttons to a separate window above IME.
+ * Sets whether the taskbar window should be focusable, as well as IME focusable. If we're now
+ * focusable, also move nav buttons to a separate window above IME.
+ *
+ * @param focusable whether it should be focusable.
+ *
+ * @see WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE
*/
public void setTaskbarWindowFocusableForIme(boolean focusable) {
if (focusable) {
@@ -1275,7 +1291,7 @@
} else {
mControllers.navbarButtonsViewController.moveNavButtonsBackToTaskbarWindow();
}
- setTaskbarWindowFocusable(focusable);
+ setTaskbarWindowFocusable(focusable, true /* imeFocusable */);
}
/** Adds the given view to WindowManager with the provided LayoutParams (creates new window). */
@@ -1710,7 +1726,7 @@
}
// There is no task associated with this launch - launch a new task through an intent
ActivityOptionsWrapper opts = getActivityLaunchDesktopOptions();
- if (enableStartLaunchTransitionFromTaskbarBugfix()) {
+ if (DesktopModeFlags.ENABLE_START_LAUNCH_TRANSITION_FROM_TASKBAR_BUGFIX.isTrue()) {
mSysUiProxy.startLaunchIntentTransition(intent, opts.options.toBundle(), displayId);
} else {
startActivity(intent, opts.options.toBundle());
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index b91f512..d531e2c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -52,6 +52,7 @@
import android.window.SurfaceSyncGroup;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.app.animation.Interpolators;
import com.android.internal.logging.InstanceId;
@@ -75,6 +76,7 @@
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.DisplayController;
@@ -517,6 +519,7 @@
return mIsSystemDragInProgress;
}
+ @VisibleForTesting
private void maybeOnDragEnd() {
if (!isDragging()) {
((BubbleTextView) mDragObject.originalView).setIconDisabled(false);
@@ -524,17 +527,38 @@
TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING, false);
mActivity.onDragEnd();
if (mReturnAnimator == null) {
+ // If an item is dropped on the bubble bar, the bubble bar handles the drop,
+ // so it should not collapse along with the taskbar.
+ boolean droppedOnBubbleBar = notifyBubbleBarItemDropped();
// Upon successful drag, immediately stash taskbar.
// Note, this must be done last to ensure no AutohideSuspendFlags are active, as
// that will prevent us from stashing until the timeout.
- mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
-
+ mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
+ /* stash = */ true,
+ /* shouldBubblesFollow = */ !droppedOnBubbleBar
+ );
mActivity.getStatsLogManager().logger().withItemInfo(mDragObject.dragInfo)
.log(LAUNCHER_APP_LAUNCH_DRAGDROP);
}
}
}
+ /**
+ * Exits the Bubble Bar drop target mode if applicable.
+ *
+ * @return {@code true} if drop target mode was active.
+ */
+ private boolean notifyBubbleBarItemDropped() {
+ return mControllers.bubbleControllers.map(bc -> {
+ BubbleBarViewController bubbleBarViewController = bc.bubbleBarViewController;
+ boolean showingDropTarget = bubbleBarViewController.isShowingDropTarget();
+ if (showingDropTarget) {
+ bubbleBarViewController.onItemDroppedInBubbleBarDragZone();
+ }
+ return showingDropTarget;
+ }).orElse(false);
+ }
+
@Override
protected void endDrag() {
if (mDisallowGlobalDrag) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 46802c3..34bb6e0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -45,6 +45,7 @@
import android.os.Handler;
import android.os.Trace;
import android.provider.Settings;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -75,14 +76,20 @@
import com.android.quickstep.fallback.window.RecentsDisplayModel;
import com.android.quickstep.fallback.window.RecentsWindowManager;
import com.android.quickstep.util.ContextualSearchInvoker;
+import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.RecentsViewContainer;
+import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.statusbar.phone.BarTransitions;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
import java.io.PrintWriter;
+import java.util.Set;
import java.util.StringJoiner;
/**
@@ -194,6 +201,65 @@
recreateTaskbar();
};
+ private final PerceptibleTaskListener mTaskStackListener;
+
+ private class PerceptibleTaskListener implements TaskStackChangeListener {
+ private ArraySet<Integer> mPerceptibleTasks = new ArraySet<Integer>();
+
+ @Override
+ public void onTaskMovedToFront(int taskId) {
+ if (mPerceptibleTasks.contains(taskId)) {
+ return;
+ }
+
+ // This listens to any Task, so we filter them by the ones shown in the launcher.
+ // For Tasks restored after startup, they will by default not be Perceptible, and no
+ // need to until user interacts with it by bringing it to the foreground.
+ for (int i = 0; i < mTaskbars.size(); i++) {
+ // get pinned tasks
+ Set<Integer> taskbarPinnedTasks =
+ mTaskbars.valueAt(i).getControllers().taskbarViewController
+ .getTaskIdsForPinnedApps();
+
+ // mark as perceptible if the foregrounded task is in the list of apps shown in
+ // the launcher.
+ if (taskbarPinnedTasks.contains(taskId)
+ && ActivityManagerWrapper.getInstance()
+ .setTaskIsPerceptible(taskId, true)
+ ) {
+ mPerceptibleTasks.add(taskId);
+ }
+ }
+ }
+
+ /**
+ * Launcher also can display recently launched tasks that are not pinned. Also add
+ * these as perceptible
+ */
+ @Override
+ public void onRecentTaskListUpdated() {
+ for (int i = 0; i < mTaskbars.size(); i++) {
+ for (GroupTask gTask : mTaskbars.valueAt(i).getControllers()
+ .taskbarRecentAppsController.getShownTasks()) {
+ for (Task task : gTask.getTasks()) {
+ int taskId = task.key.id;
+
+ if (!mPerceptibleTasks.contains(taskId)) {
+ ActivityManagerWrapper.getInstance()
+ .setTaskIsPerceptible(taskId, true);
+ mPerceptibleTasks.add(taskId);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onTaskRemoved(int taskId) {
+ mPerceptibleTasks.remove(taskId);
+ }
+ };
+
private boolean mUserUnlocked = false;
private final SimpleBroadcastReceiver mTaskbarBroadcastReceiver;
@@ -296,6 +362,12 @@
mTaskbarBroadcastReceiver.register(RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
});
+ if (ActivityManagerWrapper.usePerceptibleTasks(getPrimaryWindowContext())) {
+ mTaskStackListener = new PerceptibleTaskListener();
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
+ } else {
+ mTaskStackListener = null;
+ }
debugTaskbarManager("TaskbarManager created");
recreateTaskbar();
}
@@ -709,15 +781,14 @@
return;
}
- // TODO (b/391965805): remove once onDisplayAddSystemDecorations is working.
- WindowManager wm = getWindowManager(displayId);
- if (wm == null || !wm.shouldShowSystemDecors(displayId)) {
- return;
- }
-
Context newWindowContext = createWindowContext(displayId);
if (newWindowContext != null) {
addWindowContextToMap(displayId, newWindowContext);
+ // TODO (b/391965805): remove once onDisplayAddSystemDecorations is working.
+ WindowManager wm = getWindowManager(displayId);
+ if (wm == null || !wm.shouldShowSystemDecors(displayId)) {
+ return;
+ }
createTaskbarRootLayout(displayId);
createNavButtonController(displayId);
createAndRegisterComponentCallbacks(displayId);
@@ -747,7 +818,11 @@
/**
* Signal from SysUI indicating that system decorations should be removed from the display.
*/
- public void onDisplayRemoveSystemDecorations(int displayId) {}
+ public void onDisplayRemoveSystemDecorations(int displayId) {
+ // The display mirroring starts. The handling logic is the same as when removing a
+ // display.
+ onDisplayRemoved(displayId);
+ }
private void removeActivityCallbacksAndListeners() {
if (mActivity != null) {
@@ -784,6 +859,12 @@
Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
removeAndUnregisterComponentCallbacks(getDefaultDisplayId());
mShutdownReceiver.unregisterReceiverSafely();
+ if (ActivityManagerWrapper.usePerceptibleTasks(getPrimaryWindowContext())) {
+ for (Integer taskId: mTaskStackListener.mPerceptibleTasks) {
+ ActivityManagerWrapper.getInstance().setTaskIsPerceptible(taskId, false);
+ }
+ }
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
destroyAllTaskbars();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index 249773d..97be2e8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -44,10 +44,13 @@
private val arrowVisibleHeight: Float
private val strokeAlpha: Int
+ private val strokeColor: Int
+ private val strokeColorDropTarget: Int
private val shadowAlpha: Int
private val shadowBlur: Float
private val keyShadowDistance: Float
private var arrowHeightFraction = 1f
+ private var isShowingDropTarget: Boolean = false
var arrowPositionX: Float = 0f
private set
@@ -100,7 +103,9 @@
fillPaint.flags = Paint.ANTI_ALIAS_FLAG
fillPaint.style = Paint.Style.FILL
// configure stroke paint
- strokePaint.color = context.getColor(R.color.taskbar_stroke)
+ strokeColor = context.getColor(R.color.taskbar_stroke)
+ strokeColorDropTarget = context.getColor(com.android.internal.R.color.system_primary_fixed)
+ strokePaint.color = strokeColor
strokePaint.flags = Paint.ANTI_ALIAS_FLAG
strokePaint.style = Paint.Style.STROKE
strokePaint.strokeWidth = res.getDimension(R.dimen.transient_taskbar_stroke_width)
@@ -235,9 +240,25 @@
return max(0f, getScaledArrowHeight() - (arrowHeight - arrowVisibleHeight))
}
+ /** Set whether the background should show the drop target */
+ fun showDropTarget(isDropTarget: Boolean) {
+ if (isShowingDropTarget == isDropTarget) {
+ return
+ }
+ isShowingDropTarget = isDropTarget
+ val strokeColor = if (isDropTarget) strokeColorDropTarget else strokeColor
+ val alpha = if (isDropTarget) DRAG_STROKE_ALPHA else strokeAlpha
+ strokePaint.color = strokeColor
+ strokePaint.alpha = alpha
+ invalidateSelf()
+ }
+
+ fun isShowingDropTarget() = isShowingDropTarget
+
companion object {
private const val DARK_THEME_STROKE_ALPHA = 51
private const val LIGHT_THEME_STROKE_ALPHA = 41
+ private const val DRAG_STROKE_ALPHA = 255
private const val DARK_THEME_SHADOW_ALPHA = 51
private const val LIGHT_THEME_SHADOW_ALPHA = 25
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 4e029e3..5ddbe03 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -33,7 +33,8 @@
import android.os.SystemProperties;
import android.util.ArrayMap;
import android.util.Log;
-import android.widget.Toast;
+
+import androidx.annotation.NonNull;
import com.android.launcher3.taskbar.TaskbarSharedState;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
@@ -589,15 +590,24 @@
}
@Override
- public void onDragItemOverBubbleBarDragZone(BubbleBarLocation location) {
- //TODO(b/388894910): add meaningful implementation
- MAIN_EXECUTOR.execute(() ->
- Toast.makeText(mContext, "onDragItemOver " + location, Toast.LENGTH_SHORT).show());
+ public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) {
+ MAIN_EXECUTOR.execute(() -> {
+ mBubbleBarViewController.onDragItemOverBubbleBarDragZone(bubbleBarLocation);
+ if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) {
+ mBubbleBarLocationListener.onBubbleBarLocationAnimated(bubbleBarLocation);
+ }
+ });
}
@Override
public void onItemDraggedOutsideBubbleBarDropZone() {
-
+ MAIN_EXECUTOR.execute(() -> {
+ if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) {
+ BubbleBarLocation original = mBubbleBarViewController.getBubbleBarLocation();
+ mBubbleBarLocationListener.onBubbleBarLocationAnimated(original);
+ }
+ mBubbleBarViewController.onItemDraggedOutsideBubbleBarDropZone();
+ });
}
/** Notifies WMShell to show the expanded view. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index c001123..d43ebe2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -536,6 +536,16 @@
return (float) (displayWidth - getWidth() - margin);
}
+ /** Set whether the background should show the drop target */
+ public void showDropTarget(boolean isDropTarget) {
+ mBubbleBarBackground.showDropTarget(isDropTarget);
+ }
+
+ /** Returns whether the Bubble Bar is currently displaying a drop target. */
+ public boolean isShowingDropTarget() {
+ return mBubbleBarBackground.isShowingDropTarget();
+ }
+
/**
* Animate bubble bar to the given location transiently. Does not modify the layout or the value
* returned by {@link #getBubbleBarLocation()}.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 026f239..b90a5b0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -131,12 +131,13 @@
// Whether the bar is hidden when stashed
private boolean mHiddenForStashed;
private boolean mShouldShowEducation;
-
public boolean mOverflowAdded;
+ private boolean mIsLocationUpdatedForDropTarget = false;
private BubbleBarViewAnimator mBubbleBarViewAnimator;
private final FrameLayout mBubbleBarContainer;
private BubbleBarFlyoutController mBubbleBarFlyoutController;
+ private BubbleBarPinController mBubbleBarPinController;
private TaskbarSharedState mTaskbarSharedState;
private final TimeSource mTimeSource = System::currentTimeMillis;
private final int mTaskbarTranslationDelta;
@@ -166,6 +167,7 @@
mBubbleStashController = bubbleControllers.bubbleStashController;
mBubbleBarController = bubbleControllers.bubbleBarController;
mBubbleDragController = bubbleControllers.bubbleDragController;
+ mBubbleBarPinController = bubbleControllers.bubbleBarPinController;
mTaskbarStashController = controllers.taskbarStashController;
mTaskbarInsetsController = controllers.taskbarInsetsController;
mBubbleBarFlyoutController = new BubbleBarFlyoutController(
@@ -274,7 +276,10 @@
@Override
public boolean isOnLeft() {
- return mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl());
+ boolean shouldRevertLocation =
+ mBarView.isShowingDropTarget() && mIsLocationUpdatedForDropTarget;
+ boolean isOnLeft = mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl());
+ return shouldRevertLocation != isOnLeft;
}
@Override
@@ -524,6 +529,61 @@
mBarView.animateToBubbleBarLocation(bubbleBarLocation);
}
+ /** Returns whether the Bubble Bar is currently displaying a drop target. */
+ public boolean isShowingDropTarget() {
+ return mBarView.isShowingDropTarget();
+ }
+
+ /**
+ * Notifies the controller that a drag event is over the Bubble Bar drop zone. The controller
+ * will display the appropriate drop target and enter drop target mode. The controller will also
+ * update the return value of {@link #isLocationUpdatedForDropTarget()} to true if location was
+ * updated.
+ */
+ public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) {
+ mBarView.showDropTarget(/* isDropTarget = */ true);
+ mIsLocationUpdatedForDropTarget = getBubbleBarLocation() != bubbleBarLocation;
+ if (mIsLocationUpdatedForDropTarget) {
+ animateBubbleBarLocation(bubbleBarLocation);
+ }
+ if (!hasBubbles()) {
+ mBubbleBarPinController.showDropTarget(bubbleBarLocation);
+ }
+ }
+
+ /**
+ * Returns {@code true} if location was updated after most recent
+ * {@link #onDragItemOverBubbleBarDragZone}}.
+ */
+ public boolean isLocationUpdatedForDropTarget() {
+ return mIsLocationUpdatedForDropTarget;
+ }
+
+ /**
+ * Notifies the controller that the drag event is outside the Bubble Bar drop zone.
+ * This will hide the drop target zone if there are no bubbles or return the
+ * Bubble Bar to its original location. The controller will also exit drop target
+ * mode and reset the value returned from {@link #isLocationUpdatedForDropTarget()} to false.
+ */
+ public void onItemDraggedOutsideBubbleBarDropZone() {
+ mBarView.showDropTarget(/* isDropTarget = */ false);
+ if (mIsLocationUpdatedForDropTarget) {
+ animateBubbleBarLocation(getBubbleBarLocation());
+ }
+ mBubbleBarPinController.hideDropTarget();
+ mIsLocationUpdatedForDropTarget = false;
+ }
+
+ /**
+ * Notifies the controller that the drag has completed over the Bubble Bar drop zone.
+ * The controller will hide the drop target if there are no bubbles and exit drop target mode.
+ */
+ public void onItemDroppedInBubbleBarDragZone() {
+ mBarView.showDropTarget(/* isDropTarget = */ false);
+ mBubbleBarPinController.hideDropTarget();
+ mIsLocationUpdatedForDropTarget = false;
+ }
+
/**
* The bounds of the bubble bar.
*/
@@ -996,7 +1056,12 @@
boolean isInApp = mTaskbarStashController.isInApp();
// if this is the first bubble, animate to the initial state.
if (mBarView.getBubbleChildCount() == 1 && !isUpdate) {
- mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
+ // If a drop target is visible and the first bubble is added, hide the empty drop target
+ if (mBarView.isShowingDropTarget()) {
+ mBubbleBarPinController.hideDropTarget();
+ }
+ mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding,
+ mBarView.isShowingDropTarget());
return;
}
// if we're not stashed or we're in persistent taskbar, animate for collapsed state.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 745c689..30cfafe 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -365,7 +365,12 @@
}
/** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
- fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
+ fun animateToInitialState(
+ b: BubbleBarBubble,
+ isInApp: Boolean,
+ isExpanding: Boolean,
+ isDragging: Boolean = false,
+ ) {
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
@@ -374,15 +379,12 @@
// bubble bar to the handle if we're in an app.
val showAnimation = buildBubbleBarSpringInAnimation()
val hideAnimation =
- if (isInApp && !isExpanding) {
+ if (isInApp && !isExpanding && !isDragging) {
buildBubbleBarToHandleAnimation()
} else {
Runnable {
- moveToState(AnimatingBubble.State.ANIMATING_OUT)
- bubbleBarFlyoutController.collapseFlyout {
- onFlyoutRemoved()
- clearAnimatingBubble()
- }
+ collapseFlyoutAndUpdateState()
+ if (isDragging) return@Runnable
bubbleStashController.showBubbleBarImmediate()
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -440,11 +442,7 @@
// first bounce the bubble bar and show the flyout. Then hide the flyout.
val showAnimation = buildBubbleBarBounceAnimation()
val hideAnimation = Runnable {
- moveToState(AnimatingBubble.State.ANIMATING_OUT)
- bubbleBarFlyoutController.collapseFlyout {
- onFlyoutRemoved()
- clearAnimatingBubble()
- }
+ collapseFlyoutAndUpdateState()
bubbleStashController.showBubbleBarImmediate()
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -454,6 +452,14 @@
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
+ private fun collapseFlyoutAndUpdateState() {
+ moveToState(AnimatingBubble.State.ANIMATING_OUT)
+ bubbleBarFlyoutController.collapseFlyout {
+ onFlyoutRemoved()
+ clearAnimatingBubble()
+ }
+ }
+
/**
* The bubble bar animation when it is collapsed is divided into 2 chained animations. The first
* animation is a regular accelerate animation that moves the bubble bar upwards. When it ends
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 37b8dc7..15a27d1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -17,6 +17,7 @@
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
+import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -53,7 +54,6 @@
import com.android.launcher3.celllayout.DelegatedCellDrawing;
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.FastBitmapDrawable;
-import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -138,7 +138,7 @@
public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
- mNormalizedIconSize = IconNormalizer.getNormalizedCircleSize(getIconSize());
+ mNormalizedIconSize = Math.round(getIconSize() * ICON_VISIBLE_AREA_FACTOR);
int shadowSize = context.getResources().getDimensionPixelSize(
R.dimen.blur_size_thin_outline);
mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
index c8f46a9..cca8bf8 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
@@ -20,6 +20,7 @@
import com.android.app.animation.Interpolators.FINAL_FRAME
import com.android.app.animation.Interpolators.INSTANT
import com.android.app.animation.Interpolators.LINEAR
+import com.android.launcher3.Flags.enableDesktopExplodedView
import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
import com.android.launcher3.LauncherState
import com.android.launcher3.anim.AnimatedFloat
@@ -51,6 +52,7 @@
import com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION
import com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION
import com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA
+import com.android.quickstep.views.RecentsViewUtils.Companion.DESK_EXPLODE_PROGRESS
import com.android.quickstep.views.TaskView.Companion.FLAG_UPDATE_ALL
/**
@@ -74,6 +76,13 @@
recentsView,
if (state.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f,
)
+ if (enableDesktopExplodedView()) {
+ DESK_EXPLODE_PROGRESS.set(
+ recentsView,
+ if (state.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f,
+ )
+ }
+
TASK_THUMBNAIL_SPLASH_ALPHA.set(
recentsView,
if (state.showTaskThumbnailSplash()) 1f else 0f,
@@ -156,6 +165,15 @@
getOverviewInterpolator(fromState, toState),
)
+ if (enableDesktopExplodedView()) {
+ builder.setFloat(
+ recentsView,
+ DESK_EXPLODE_PROGRESS,
+ if (toState.isRecentsViewVisible) 1f else 0f,
+ getOverviewInterpolator(fromState, toState),
+ )
+ }
+
if (enableLargeDesktopWindowingTile()) {
builder.setFloat(
recentsView,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
index 6e901ee..1f34969 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
@@ -144,7 +144,7 @@
override fun isNonResizeableActivity(lai: LauncherActivityInfo) =
lai.activityInfo.resizeMode == ActivityInfo.RESIZE_MODE_UNRESIZEABLE
- override fun supportsMultiInstance(lai: LauncherActivityInfo) : Boolean {
+ override fun supportsMultiInstance(lai: LauncherActivityInfo): Boolean {
return try {
super.supportsMultiInstance(lai) || lai.supportsMultiInstance()
} catch (e: Exception) {
@@ -202,4 +202,7 @@
(appInfo.sourceDir?.hashCode() ?: 0).toString() + " " + appInfo.longVersionCode
override fun getRoundIconRes(appInfo: ApplicationInfo) = appInfo.roundIconRes
+
+ override fun isFileDrawable(shortcutInfo: ShortcutInfo) =
+ shortcutInfo.hasIconFile() || shortcutInfo.hasIconUri()
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index 7c09e9a..0d2cfbf 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -317,6 +317,12 @@
)
addPreference(
Preference(context).apply {
+ title = "Launch Full Gesture Tutorial"
+ intent = Intent(launchSandboxIntent).putExtra("use_tutorial_menu", false)
+ }
+ )
+ addPreference(
+ Preference(context).apply {
title = "Launch Back Tutorial"
intent =
Intent(launchSandboxIntent)
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 10513c0..b27c6e8 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -153,7 +153,7 @@
return new PageAlphaProvider(DECELERATE_2) {
@Override
public float getPageAlpha(int pageIndex) {
- return launcher.getDeviceProfile().shouldShowAllAppsOnSheet()
+ return launcher.getDeviceProfile().isTablet
? superPageAlphaProvider.getPageAlpha(pageIndex)
: 0;
}
@@ -164,7 +164,7 @@
public int getVisibleElements(Launcher launcher) {
int elements = ALL_APPS_CONTENT | FLOATING_SEARCH_BAR;
// When All Apps is presented on a bottom sheet, HOTSEAT_ICONS are visible.
- if (launcher.getDeviceProfile().shouldShowAllAppsOnSheet()) {
+ if (launcher.getDeviceProfile().isTablet) {
elements |= HOTSEAT_ICONS;
}
return elements;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 05d12c3..58b274a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -64,6 +64,7 @@
import android.graphics.PointF;
import android.view.MotionEvent;
import android.view.animation.Interpolator;
+import android.window.DesktopModeFlags;
import com.android.internal.jank.Cuj;
import com.android.launcher3.LauncherState;
@@ -86,7 +87,6 @@
import com.android.quickstep.util.WorkspaceRevealAnim;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
-import com.android.window.flags.Flags;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
/**
@@ -188,7 +188,7 @@
}
if (DesktopModeStatus.canEnterDesktopMode(mLauncher)
//TODO(b/345296916): replace with dev option once in teamfood
- && Flags.enableQuickswitchDesktopSplitBugfix()
+ && DesktopModeFlags.ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX.isTrue()
&& mRecentsView.getNonDesktopTaskViewCount() < 1) {
return false;
}
diff --git a/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt b/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
index bafb0b2..444e77d 100644
--- a/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
+++ b/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
@@ -17,7 +17,7 @@
package com.android.quickstep
import android.content.Context
-import com.android.systemui.shared.system.QuickStepContract
+import com.android.launcher3.R
// DesktopTaskView thumbnail's corner radius is independent of fullscreenProgress.
open class DesktopFullscreenDrawParams
@@ -28,6 +28,6 @@
// computeCornerRadius is used as cornerRadiusProvider, so
// QuickStepContract::getWindowCornerRadius can be mocked properly.
private fun computeCornerRadius(context: Context): Float =
- QuickStepContract.getWindowCornerRadius(context)
+ context.resources.getDimension(R.dimen.desktop_windowing_freeform_rounded_corner_radius)
}
}
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index be0a339..783ec2c 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -514,7 +514,13 @@
private void finishAnimation() {
mLauncher.setPredictiveBackToHomeInProgress(false);
+ if (mBackTarget != null && mBackTarget.leash.isValid()) {
+ mBackTarget.leash.release();
+ }
mBackTarget = null;
+ if (mLauncherTarget != null && mLauncherTarget.leash.isValid()) {
+ mLauncherTarget.leash.release();
+ }
mLauncherTarget = null;
mBackInProgress = false;
mBackProgress = 0;
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 42aa86e..afdb403 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -293,10 +293,6 @@
val recentsView: RecentsView<*, *>? = recentsViewContainer?.getOverviewPanel()
val deviceProfile = recentsViewContainer?.getDeviceProfile()
val uiController = containerInterface.getTaskbarController()
- val allowQuickSwitch =
- uiController != null &&
- deviceProfile != null &&
- (deviceProfile.isTablet || deviceProfile.isTwoPanels)
val focusedDisplayId = focusState.focusedDisplayId
val focusedDisplayUIController: TaskbarUIController? =
@@ -322,26 +318,26 @@
when (command.type) {
HIDE -> {
- if (!allowQuickSwitch) return true
+ if (uiController == null || deviceProfile?.isTablet == false) return true
keyboardTaskFocusIndex =
if (
enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
) {
focusedDisplayUIController.launchFocusedTask()
} else {
- uiController!!.launchFocusedTask()
+ uiController.launchFocusedTask()
}
if (keyboardTaskFocusIndex == -1) return true
}
KEYBOARD_INPUT ->
- if (allowQuickSwitch) {
+ if (uiController != null && deviceProfile?.isTablet == true) {
if (
enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
) {
focusedDisplayUIController.openQuickSwitchView()
} else {
- uiController!!.openQuickSwitchView()
+ uiController.openQuickSwitchView()
}
return true
} else {
@@ -365,7 +361,11 @@
TOGGLE -> {}
}
- recentsView?.setKeyboardTaskFocusIndex(keyboardTaskFocusIndex)
+ recentsView?.setKeyboardTaskFocusIndex(
+ recentsView.indexOfChild(recentsView.taskViews.elementAtOrNull(keyboardTaskFocusIndex))
+ ?: -1
+ )
+
// Handle recents view focus when launching from home
val animatorListener: Animator.AnimatorListener =
object : AnimatorListenerAdapter() {
@@ -526,7 +526,7 @@
// Stops requesting focused after first view gets focused.
recentsView.getTaskViewAt(keyboardTaskFocusIndex).requestFocus() ||
recentsView.nextTaskView.requestFocus() ||
- recentsView.getFirstTaskView().requestFocus() ||
+ recentsView.firstTaskView.requestFocus() ||
recentsView.requestFocus()
}
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 9810308..cb11afa 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -286,8 +286,32 @@
mCallbacks.addListener(listener);
final ActivityOptions options = ActivityOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
+ options.setTransientLaunch();
+ options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
- // TODO:(b/365777482) if flag is enabled, but on launcher it will crash.
+ // Notify taskbar that we should skip reacting to launcher visibility change to
+ // avoid a jumping taskbar.
+ TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
+ if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
+ taskbarUIController.setSkipLauncherVisibilityChange(true);
+
+ mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
+ @Override
+ public void onRecentsAnimationCanceled(
+ @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
+ taskbarUIController.setSkipLauncherVisibilityChange(false);
+ }
+
+ @Override
+ public void onRecentsAnimationFinished(
+ @NonNull RecentsAnimationController controller) {
+ taskbarUIController.setSkipLauncherVisibilityChange(false);
+ }
+ });
+ }
+
if(containerInterface.getCreatedContainer() instanceof RecentsWindowManager
&& (Flags.enableFallbackOverviewInWindow()
|| Flags.enableLauncherOverviewInWindow())) {
@@ -297,32 +321,6 @@
.getRecentsWindowManager(mDeviceState.getDisplayId())
.startRecentsWindow(mCallbacks);
} else {
- options.setPendingIntentBackgroundActivityStartMode(
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
- options.setTransientLaunch();
- options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
-
- // Notify taskbar that we should skip reacting to launcher visibility change to
- // avoid a jumping taskbar.
- TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
- if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
- taskbarUIController.setSkipLauncherVisibilityChange(true);
-
- mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
- @Override
- public void onRecentsAnimationCanceled(
- @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
- taskbarUIController.setSkipLauncherVisibilityChange(false);
- }
-
- @Override
- public void onRecentsAnimationFinished(
- @NonNull RecentsAnimationController controller) {
- taskbarUIController.setSkipLauncherVisibilityChange(false);
- }
- });
- }
-
mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent,
options, mCallbacks, false /* useSyntheticRecentsTransition */);
}
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index b4b80c5..56dd696 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -18,6 +18,7 @@
import static com.android.app.animation.Interpolators.FINAL_FRAME;
import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.Flags.enableDesktopExplodedView;
import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
@@ -36,6 +37,7 @@
import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION;
import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA;
+import static com.android.quickstep.views.RecentsViewUtils.DESK_EXPLODE_PROGRESS;
import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL;
import android.util.FloatProperty;
@@ -123,6 +125,10 @@
state.detachDesktopCarousel() ? 1f : 0f,
getOverviewInterpolator(state));
}
+ if (enableDesktopExplodedView()) {
+ setter.setFloat(mRecentsView, DESK_EXPLODE_PROGRESS, showAsGrid ? 1f : 0f,
+ getOverviewInterpolator(state));
+ }
setter.setViewBackgroundColor(mRecentsViewContainer.getScrimView(),
state.getScrimColor(mRecentsViewContainer.asContext()),
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
index 047658c..cd48136 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
@@ -61,9 +61,13 @@
return dragLayer
}
+ fun initDeviceProfile() {
+ deviceProfile = InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this)
+ }
+
override fun getDeviceProfile(): DeviceProfile {
if (deviceProfile == null) {
- deviceProfile = InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this).copy(this)
+ initDeviceProfile()
}
return deviceProfile!!
}
@@ -79,7 +83,10 @@
* @param type The window type to pass to the created WindowManager.LayoutParams.
* @param title The window title to pass to the created WindowManager.LayoutParams.
*/
- fun createDefaultWindowLayoutParams(type: Int, title: String): WindowManager.LayoutParams {
+ private fun createDefaultWindowLayoutParams(
+ type: Int,
+ title: String,
+ ): WindowManager.LayoutParams {
var windowFlags =
(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index 07288d8..1f4961a 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -21,6 +21,7 @@
import android.content.ComponentName
import android.content.Context
import android.content.LocusId
+import android.content.res.Configuration
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
@@ -32,6 +33,7 @@
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
import android.window.RemoteTransition
+import com.android.launcher3.AbstractFloatingView
import com.android.launcher3.BaseActivity
import com.android.launcher3.LauncherAnimationRunner
import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory
@@ -135,73 +137,6 @@
listOf(RunnableList(), RunnableList(), RunnableList(), RunnableList())
private var onInitListener: Predicate<Boolean>? = null
- private val taskStackChangeListener =
- object : TaskStackChangeListener {
- override fun onTaskMovedToFront(taskId: Int) {
- if ((isShowing() && isInState(DEFAULT))) {
- // handling state where we end recents animation by swiping livetile away
- // TODO: animate this switch.
- cleanupRecentsWindow()
- }
- }
- }
-
- private val recentsAnimationListener =
- object : RecentsAnimationListener {
- override fun onRecentsAnimationCanceled(thumbnailDatas: HashMap<Int, ThumbnailData>) {
- recentAnimationStopped()
- }
-
- override fun onRecentsAnimationFinished(controller: RecentsAnimationController) {
- recentAnimationStopped()
- }
- }
-
- init {
- TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener)
- }
-
- override fun destroy() {
- super.destroy()
- cleanupRecentsWindow()
- TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
- callbacks?.removeListener(recentsAnimationListener)
- recentsWindowTracker.onContextDestroyed(this)
- recentsView?.destroy()
- }
-
- override fun startHome() {
- startHome(/* finishRecentsAnimation= */ true)
- }
-
- fun startHome(finishRecentsAnimation: Boolean) {
- val recentsView: RecentsView<*, *> = getOverviewPanel()
-
- if (!finishRecentsAnimation) {
- recentsView.switchToScreenshot(/* onFinishRunnable= */ null)
- startHomeInternal()
- return
- }
- recentsView.switchToScreenshot {
- recentsView.finishRecentsAnimation(/* toRecents= */ true) { startHomeInternal() }
- }
- }
-
- private fun startHomeInternal() {
- val runner = LauncherAnimationRunner(mainThreadHandler, animationToHomeFactory, true)
- val options =
- ActivityOptions.makeRemoteAnimation(
- RemoteAnimationAdapter(runner, HOME_APPEAR_DURATION, 0),
- RemoteTransition(
- runner.toRemoteTransition(),
- iApplicationThread,
- "StartHomeFromRecents",
- ),
- )
- OverviewComponentObserver.startHomeIntentSafely(this, options.toBundle(), TAG)
- stateManager.moveToRestState()
- }
-
private val animationToHomeFactory =
RemoteAnimationFactory {
_: Int,
@@ -229,7 +164,7 @@
anim,
this@RecentsWindowManager,
{
- getStateManager().goToState(BG_LAUNCHER, false)
+ getStateManager().goToState(BG_LAUNCHER, true)
cleanupRecentsWindow()
},
true, /* skipFirstFrame */
@@ -242,17 +177,49 @@
TestLogging.recordEvent(SEQUENCE_MAIN, "onBackInvoked")
}
- private fun cleanupRecentsWindow() {
- RecentsWindowProtoLogProxy.logCleanup(isShowing())
- if (isShowing()) {
- windowManager.removeViewImmediate(windowView)
+ private val taskStackChangeListener =
+ object : TaskStackChangeListener {
+ override fun onTaskMovedToFront(taskId: Int) {
+ if ((isShowing() && isInState(DEFAULT))) {
+ // handling state where we end recents animation by swiping livetile away
+ // TODO: animate this switch.
+ cleanupRecentsWindow()
+ }
+ }
}
- stateManager.moveToRestState()
- callbacks?.removeListener(recentsAnimationListener)
+
+ private val recentsAnimationListener =
+ object : RecentsAnimationListener {
+ override fun onRecentsAnimationCanceled(thumbnailDatas: HashMap<Int, ThumbnailData>) {
+ recentAnimationStopped()
+ }
+
+ override fun onRecentsAnimationFinished(controller: RecentsAnimationController) {
+ recentAnimationStopped()
+ }
+ }
+
+ init {
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener)
}
- private fun isShowing(): Boolean {
- return windowView?.parent != null
+ override fun handleConfigurationChanged(configuration: Configuration?) {
+ initDeviceProfile()
+ AbstractFloatingView.closeOpenViews(
+ this,
+ true,
+ AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv(),
+ )
+ dispatchDeviceProfileChanged()
+ }
+
+ override fun destroy() {
+ super.destroy()
+ cleanupRecentsWindow()
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
+ callbacks?.removeListener(recentsAnimationListener)
+ recentsWindowTracker.onContextDestroyed(this)
+ recentsView?.destroy()
}
fun startRecentsWindow(callbacks: RecentsAnimationCallbacks? = null) {
@@ -301,6 +268,51 @@
callbacks?.addListener(recentsAnimationListener)
}
+ override fun startHome() {
+ startHome(/* finishRecentsAnimation= */ true)
+ }
+
+ fun startHome(finishRecentsAnimation: Boolean) {
+ val recentsView: RecentsView<*, *> = getOverviewPanel()
+
+ if (!finishRecentsAnimation) {
+ recentsView.switchToScreenshot /* onFinishRunnable= */ {}
+ startHomeInternal()
+ return
+ }
+ recentsView.switchToScreenshot {
+ recentsView.finishRecentsAnimation(/* toRecents= */ true) { startHomeInternal() }
+ }
+ }
+
+ private fun startHomeInternal() {
+ val runner = LauncherAnimationRunner(mainThreadHandler, animationToHomeFactory, true)
+ val options =
+ ActivityOptions.makeRemoteAnimation(
+ RemoteAnimationAdapter(runner, HOME_APPEAR_DURATION, 0),
+ RemoteTransition(
+ runner.toRemoteTransition(),
+ iApplicationThread,
+ "StartHomeFromRecents",
+ ),
+ )
+ OverviewComponentObserver.startHomeIntentSafely(this, options.toBundle(), TAG)
+ stateManager.moveToRestState()
+ }
+
+ private fun cleanupRecentsWindow() {
+ RecentsWindowProtoLogProxy.logCleanup(isShowing())
+ if (isShowing()) {
+ windowManager.removeViewImmediate(windowView)
+ }
+ stateManager.moveToRestState()
+ callbacks?.removeListener(recentsAnimationListener)
+ }
+
+ private fun isShowing(): Boolean {
+ return windowView?.parent != null
+ }
+
private fun recentAnimationStopped() {
if (isInState(BACKGROUND_APP)) {
cleanupRecentsWindow()
@@ -352,7 +364,6 @@
override fun onStateSetEnd(state: RecentsState) {
super.onStateSetEnd(state)
RecentsWindowProtoLogProxy.logOnStateSetEnd(getStateName(state))
-
if (state == HOME || state == BG_LAUNCHER) {
cleanupRecentsWindow()
}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index afe988d..a703c23 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DEEP_PRESS_STASHED_TASKBAR;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_NAVBAR;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_STASHED_TASKBAR;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.LogConfig.NAV_HANDLE_LONG_PRESS;
@@ -30,6 +31,7 @@
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.Utilities;
+import com.android.launcher3.logging.InstanceIdSequence;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.util.DisplayController;
import com.android.quickstep.DeviceConfigWrapper;
@@ -49,6 +51,8 @@
private static final String TAG = "NavHandleLongPressIC";
private static final boolean DEBUG_NAV_HANDLE = Utilities.isPropertyEnabled(
NAV_HANDLE_LONG_PRESS);
+ // Minimum time between touch down and abandon to log.
+ @VisibleForTesting static final long MIN_TIME_TO_LOG_ABANDON_MS = 200;
private NavHandleLongPressHandler mNavHandleLongPressHandler;
private final float mNavHandleWidth;
@@ -62,11 +66,12 @@
private final int mOuterLongPressTimeout;
private final boolean mDeepPressEnabled;
private final NavHandle mNavHandle;
- private final StatsLogManager mStatsLogManager;
+ private StatsLogManager mStatsLogManager;
private final TopTaskTracker mTopTaskTracker;
private final GestureState mGestureState;
- private MotionEvent mCurrentDownEvent;
+ private MotionEvent mCurrentDownEvent; // Down event that started the current gesture.
+ private MotionEvent mCurrentMotionEvent; // Most recent motion event.
private boolean mDeepPressLogged; // Whether deep press has been logged for the current touch.
public NavHandleLongPressInputConsumer(Context context, InputConsumer delegate,
@@ -125,6 +130,10 @@
@Override
public void onMotionEvent(MotionEvent ev) {
+ if (mCurrentMotionEvent != null) {
+ mCurrentMotionEvent.recycle();
+ }
+ mCurrentMotionEvent = MotionEvent.obtain(ev);
if (mDelegate.allowInterceptByParent()) {
handleMotionEvent(ev);
} else if (MAIN_EXECUTOR.getHandler().hasCallbacks(mTriggerLongPress)) {
@@ -244,6 +253,15 @@
if (DEBUG_NAV_HANDLE) {
Log.d(TAG, "cancelLongPress: " + reason);
}
+ // Log LPNH abandon latency if we didn't trigger but were still prepared to.
+ long latencyMs = mCurrentMotionEvent.getEventTime() - mCurrentDownEvent.getEventTime();
+ if (mState != STATE_ACTIVE && MAIN_EXECUTOR.getHandler().hasCallbacks(mTriggerLongPress)
+ && latencyMs >= MIN_TIME_TO_LOG_ABANDON_MS) {
+ mStatsLogManager.latencyLogger()
+ .withInstanceId(new InstanceIdSequence().newInstanceId())
+ .withLatency(latencyMs)
+ .log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
+ }
mGestureState.setIsInExtendedSlopRegion(false);
MAIN_EXECUTOR.getHandler().removeCallbacks(mTriggerLongPress);
mNavHandleLongPressHandler.onTouchFinished(mNavHandle, reason);
@@ -274,4 +292,9 @@
void setNavHandleLongPressHandler(NavHandleLongPressHandler navHandleLongPressHandler) {
mNavHandleLongPressHandler = navHandleLongPressHandler;
}
+
+ @VisibleForTesting
+ void setStatsLogManager(StatsLogManager statsLogManager) {
+ mStatsLogManager = statsLogManager;
+ }
}
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index c986b88..953b0c5 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -92,6 +92,8 @@
private static final String LOG_TAG = "AllSetActivity";
private static final String URI_SYSTEM_NAVIGATION_SETTING =
"#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S.:settings:fragment_args_key=gesture_system_navigation_input_summary;S.:settings:show_fragment=com.android.settings.gestures.SystemNavigationGestureSettings;end";
+ private static final String INTENT_ACTION_ACTIVITY_CLOSED =
+ "com.android.quickstep.interaction.ACTION_ALL_SET_ACTIVITY_CLOSED";
private static final String EXTRA_ACCENT_COLOR_DARK_MODE = "suwColorAccentDark";
private static final String EXTRA_ACCENT_COLOR_LIGHT_MODE = "suwColorAccentLight";
private static final String EXTRA_DEVICE_NAME = "suwDeviceName";
@@ -332,6 +334,7 @@
mLauncherStartAnim.dispatchOnEnd();
mLauncherStartAnim = null;
}
+ sendBroadcast(new Intent(INTENT_ACTION_ACTIVITY_CLOSED));
}
@Override
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
index 608fafd..12616a8 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
@@ -17,6 +17,7 @@
package com.android.quickstep.recents.data
import android.os.UserHandle
+import android.util.Log
import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
@@ -58,7 +59,7 @@
fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?)
/** Informs the listener that the default resolution for loading thumbnails has changed */
- fun onHighResLoadingStateChanged()
+ fun onHighResLoadingStateChanged(highResEnabled: Boolean)
}
}
@@ -91,8 +92,9 @@
}
override fun onHighResLoadingStateChanged(enabled: Boolean) {
+ Log.d(TAG, "onHighResLoadingStateChanged(enabled = $enabled)")
taskThumbnailChangedCallbacks.values.forEach { (_, callback) ->
- callback.onHighResLoadingStateChanged()
+ callback.onHighResLoadingStateChanged(enabled)
}
}
@@ -142,4 +144,8 @@
}
}
}
+
+ companion object {
+ const val TAG = "TaskVisualsChangedDelegateImpl"
+ }
}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index b1a5920..5274ef3 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -160,7 +160,17 @@
updateThumbnail(task.key.id, thumbnailData)
}
- override fun onHighResLoadingStateChanged() {
+ override fun onHighResLoadingStateChanged(highResEnabled: Boolean) {
+ val isTaskVisible = taskRequests.containsKey(task.key.id)
+ if (!isTaskVisible) return
+
+ val isCurrentThumbnailLowRes =
+ tasks.value[task.key.id]?.thumbnail?.reducedResolution
+ val isRequestedResHigherThanCurrent =
+ isCurrentThumbnailLowRes == null ||
+ (isCurrentThumbnailLowRes && highResEnabled)
+ if (!isRequestedResHigherThanCurrent) return
+
recentsCoroutineScope.launch(dispatcherProvider.background) {
updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index e672ec4..e91073a 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -20,6 +20,7 @@
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Outline
+import android.graphics.Path
import android.graphics.Rect
import android.graphics.drawable.ShapeDrawable
import android.util.AttributeSet
@@ -52,6 +53,7 @@
private val dimAlpha: MultiPropertyFactory<View> by lazy {
MultiPropertyFactory(scrimView, VIEW_ALPHA, ScrimViewAlpha.entries.size, ::maxOf)
}
+ private val outlinePath = Path()
private var onSizeChanged: ((width: Int, height: Int) -> Unit)? = null
private var taskThumbnailViewHeader: TaskThumbnailViewHeader? = null
@@ -96,7 +98,20 @@
outlineProvider =
object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
- outline.setRoundRect(outlineBounds ?: bounds, cornerRadius)
+ val outlineRect = outlineBounds ?: bounds
+ outlinePath.apply {
+ rewind()
+ addRoundRect(
+ outlineRect.left.toFloat(),
+ outlineRect.top.toFloat(),
+ outlineRect.right.toFloat(),
+ outlineRect.bottom.toFloat(),
+ cornerRadius / scaleX,
+ cornerRadius / scaleY,
+ Path.Direction.CW,
+ )
+ }
+ outline.setPath(outlinePath)
}
}
}
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index f20d7a5..8385485 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -26,6 +26,7 @@
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.systemui.shared.recents.utilities.Utilities.isFreeformTask;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE;
import static com.android.wm.shell.shared.split.SplitScreenConstants.getIndex;
@@ -69,6 +70,7 @@
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
import java.util.Arrays;
@@ -337,10 +339,12 @@
* c) App B is on-screen, but App A isn't.
* d) Neither is on-screen.
*
- * If the user tapped an app pair while inside a single app, there are 3 cases:
- * a) The on-screen app is App A of the app pair.
- * b) The on-screen app is App B of the app pair.
- * c) It is neither.
+ * If the user tapped an app pair while a fullscreen or freeform app is visible on screen,
+ * there are 4 cases:
+ * a) At least one of the apps in the app pair is in freeform windowing mode.
+ * b) The on-screen app is App A of the app pair.
+ * c) The on-screen app is App B of the app pair.
+ * d) It is neither.
*
* For each case, we call the appropriate animation and split launch type.
*/
@@ -422,6 +426,14 @@
foundTasks -> {
Task foundTask1 = foundTasks[0];
Task foundTask2 = foundTasks[1];
+
+ if (DesktopModeStatus.canEnterDesktopMode(context) && (isFreeformTask(
+ foundTask1) || isFreeformTask(foundTask2))) {
+ launchAppPair(launchingIconView,
+ CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR);
+ return;
+ }
+
boolean task1IsOnScreen;
boolean task2IsOnScreen;
if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 7d5b471..d2f9652 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -18,6 +18,7 @@
import android.animation.Animator;
import android.animation.RectEvaluator;
+import android.app.PictureInPictureParams;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
@@ -25,6 +26,7 @@
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Log;
+import android.util.Rational;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.View;
@@ -50,8 +52,6 @@
private static final float END_PROGRESS = 1.0f;
- private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.01f;
-
private final int mTaskId;
private final ActivityInfo mActivityInfo;
private final SurfaceControl mLeash;
@@ -158,9 +158,8 @@
// not a valid rectangle to use for cropping app surface
reasonForCreateOverlay = "Source rect hint exceeds display bounds " + sourceRectHint;
sourceRectHint.setEmpty();
- } else if (Math.abs(
- aspectRatio - (sourceRectHint.width() / (float) sourceRectHint.height()))
- > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) {
+ } else if (!PictureInPictureParams.isSameAspectRatio(sourceRectHint,
+ new Rational(destinationBounds.width(), destinationBounds.height()))) {
// The source rect hint does not aspect ratio
reasonForCreateOverlay = "Source rect hint does not match aspect ratio "
+ sourceRectHint + " aspect ratio " + aspectRatio;
diff --git a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
index 91e8376..6e2d469 100644
--- a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
+++ b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
@@ -16,16 +16,11 @@
package com.android.quickstep.util;
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-
-import android.app.Activity;
import android.app.ActivityManager;
import android.util.Log;
import androidx.annotation.NonNull;
-import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
-import com.android.quickstep.RecentsModel;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index fbda3b3..9bb8f3d 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -15,7 +15,6 @@
*/
package com.android.quickstep.views
-import android.animation.Animator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Matrix
@@ -36,7 +35,6 @@
import com.android.launcher3.Flags.enableOverviewIconMenu
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.R
-import com.android.launcher3.anim.AnimatedFloat
import com.android.launcher3.testing.TestLogging
import com.android.launcher3.testing.shared.TestProtocol
import com.android.launcher3.util.RunnableList
@@ -165,9 +163,6 @@
?.inflate()
}
- fun startWindowExplodeAnimation(): Animator =
- AnimatedFloat { progress -> explodeProgress = progress }.animateToValue(0.0f, 1.0f)
-
private fun positionTaskWindows() {
if (taskContainers.isEmpty()) {
return
diff --git a/quickstep/src/com/android/quickstep/views/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt
index 6da52d6..cb69b22 100644
--- a/quickstep/src/com/android/quickstep/views/IconView.kt
+++ b/quickstep/src/com/android/quickstep/views/IconView.kt
@@ -78,7 +78,8 @@
override fun setDrawable(d: Drawable?) {
drawable?.callback = null
- drawable = d
+ // Copy drawable so that mutations below do not affect other users of the drawable
+ drawable = d?.constantState?.newDrawable()?.mutate()
drawable?.let {
it.callback = this
setDrawableSizeInternal(width, height)
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 8100f25..9e53f11 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -38,6 +38,7 @@
import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
import static com.android.launcher3.Flags.enableDesktopExplodedView;
import static com.android.launcher3.Flags.enableDesktopTaskAlphaAnimation;
+import static com.android.launcher3.Flags.enableExpressiveDismissTaskMotion;
import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
@@ -72,6 +73,7 @@
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SELECT_ACTIVE;
+import static com.android.quickstep.views.RecentsViewUtils.DESK_EXPLODE_PROGRESS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -157,6 +159,7 @@
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.statemanager.StatefulContainer;
@@ -179,6 +182,7 @@
import com.android.launcher3.util.VibratorWrapper;
import com.android.launcher3.util.ViewPool;
import com.android.launcher3.util.coroutines.DispatcherProvider;
+import com.android.launcher3.util.window.WindowManagerProxy.DesktopVisibilityListener;
import com.android.quickstep.BaseContainerInterface;
import com.android.quickstep.GestureState;
import com.android.quickstep.HighResLoadingState;
@@ -265,7 +269,7 @@
CONTAINER_TYPE extends Context & RecentsViewContainer & StatefulContainer<STATE_TYPE>,
STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
HighResLoadingState.HighResLoadingStateChangedCallback,
- TaskVisualsChangeListener {
+ TaskVisualsChangeListener, DesktopVisibilityListener {
private static final String TAG = "RecentsView";
private static final boolean DEBUG = false;
@@ -556,6 +560,10 @@
private final Rect mTaskViewDeadZoneRect = new Rect();
private final Rect mTopRowDeadZoneRect = new Rect();
private final Rect mBottomRowDeadZoneRect = new Rect();
+
+ @Nullable
+ private DesktopVisibilityController mDesktopVisibilityController = null;
+
/**
* Reflects if Recents is currently in the middle of a gesture, and if so, which tasks are
* running. If a gesture is not in progress, this will be null.
@@ -913,6 +921,8 @@
mAddDesktopButton = (AddDesktopButton) LayoutInflater.from(context).inflate(
R.layout.overview_add_desktop_button, this, false);
mAddDesktopButton.setOnClickListener(this::createDesk);
+
+ mDesktopVisibilityController = DesktopVisibilityController.INSTANCE.get(mContext);
}
mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
@@ -1227,13 +1237,16 @@
mSyncTransactionApplier = new SurfaceTransactionApplier(this);
runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
.setSyncTransactionApplier(mSyncTransactionApplier));
- RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
+ RecentsModel.INSTANCE.get(mContext).addThumbnailChangeListener(this);
mIPipAnimationListener.setActivityAndRecentsView(mContainer, this);
- SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(
+ SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener(
mIPipAnimationListener);
mOrientationState.initListeners();
mTaskOverlayFactory.initListeners();
mSplitSelectStateController.registerSplitListener(mSplitSelectionListener);
+ if (mDesktopVisibilityController != null) {
+ mDesktopVisibilityController.registerDesktopVisibilityListener(this);
+ }
}
@Override
@@ -1248,12 +1261,15 @@
runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
.setSyncTransactionApplier(null));
executeSideTaskLaunchCallback();
- RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
- SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(null);
+ RecentsModel.INSTANCE.get(mContext).removeThumbnailChangeListener(this);
+ SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener(null);
mIPipAnimationListener.setActivityAndRecentsView(null, null);
mOrientationState.destroyListeners();
mTaskOverlayFactory.removeListeners();
mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
+ if (mDesktopVisibilityController != null) {
+ mDesktopVisibilityController.unregisterDesktopVisibilityListener(this);
+ }
reset();
}
@@ -2013,7 +2029,7 @@
}
// If the list changed, maybe the focused task doesn't exist anymore.
if (newFocusedTaskView == null) {
- newFocusedTaskView = mUtils.getExpectedFocusedTask();
+ newFocusedTaskView = mUtils.getFirstNonDesktopTaskView();
}
}
setFocusedTaskViewId(
@@ -2119,15 +2135,6 @@
return mTaskViewCount;
}
- /**
- * Transverse RecentsView children to calculate the amount of DesktopTaskViews.
- *
- * @return Number of children that are instances of DesktopTaskView
- */
- private int getDesktopTaskViewCount() {
- return mUtils.getDesktopTaskViewCount();
- }
-
/** Counts {@link TaskView}s that are not {@link DesktopTaskView} instances. */
public int getNonDesktopTaskViewCount() {
return mUtils.getNonDesktopTaskViewCount();
@@ -2905,13 +2912,15 @@
}
if (enableDesktopExplodedView()) {
+ if (animatorSet == null) {
+ mUtils.setDeskExplodeProgress(1);
+ } else {
+ animatorSet.play(
+ ObjectAnimator.ofFloat(this, DESK_EXPLODE_PROGRESS, 1));
+ }
+
for (TaskView taskView : getTaskViews()) {
if (taskView instanceof DesktopTaskView desktopTaskView) {
- if (animatorSet == null) {
- desktopTaskView.setExplodeProgress(1.0f);
- } else {
- animatorSet.play(desktopTaskView.startWindowExplodeAnimation());
- }
desktopTaskView.setRemoteTargetHandles(remoteTargetHandles);
}
}
@@ -3079,7 +3088,7 @@
focusedTaskViewId = INVALID_TASK_ID;
} else if (enableLargeDesktopWindowingTile()
&& getRunningTaskView() instanceof DesktopTaskView) {
- TaskView focusedTaskView = getTaskViewAt(getDesktopTaskViewCount());
+ TaskView focusedTaskView = mUtils.getFirstNonDesktopTaskView();
focusedTaskViewId =
focusedTaskView != null ? focusedTaskView.getTaskViewId() : INVALID_TASK_ID;
} else {
@@ -3849,7 +3858,8 @@
newClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart;
}
}
- if (lastGridTaskView != null && lastGridTaskView.isVisibleToUser()) {
+ if (lastGridTaskView != null && (lastGridTaskView.isVisibleToUser() || (
+ enableExpressiveDismissTaskMotion() && lastGridTaskView == dismissedTaskView))) {
// After dismissal, animate translation of the remaining tasks to fill any gap left
// between the end of the grid and the clear all button. Only animate if the clear
// all button is visible or would become visible after dismissal.
@@ -3950,9 +3960,9 @@
int distanceFromDismissedTask = 1;
int slidingTranslation = 0;
if (isSlidingTasks) {
- int nextSnappedPage = isStagingFocusedTask
- ? indexOfChild(mUtils.getFirstSmallTaskView())
- : mUtils.getDesktopTaskViewCount();
+ int nextSnappedPage = indexOfChild(isStagingFocusedTask
+ ? mUtils.getFirstSmallTaskView()
+ : mUtils.getFirstNonDesktopTaskView());
slidingTranslation = getPagedOrientationHandler().getPrimaryScroll(this)
- getScrollForPage(nextSnappedPage);
slidingTranslation += mIsRtl ? newClearAllShortTotalWidthTranslation
@@ -3978,7 +3988,7 @@
Math.abs(i - dismissedIndex),
scrollDiff,
anim,
- splitTimings, i);
+ splitTimings);
needsCurveUpdates = true;
}
} else if (child instanceof TaskView taskView) {
@@ -4354,8 +4364,7 @@
int indexDiff,
int scrollDiffPerPage,
PendingAnimation pendingAnimation,
- SplitAnimationTimings splitTimings,
- int index) {
+ SplitAnimationTimings splitTimings) {
// No need to translate the AddDesktopButton on dismissing a TaskView, which should be
// always at the right most position, even when dismissing the last TaskView.
if (view instanceof AddDesktopButton) {
@@ -6277,12 +6286,12 @@
}
@Override
- protected int getChildVisibleSize(int index) {
- final TaskView taskView = getTaskViewAt(index);
+ protected int getChildVisibleSize(int childIndex) {
+ final TaskView taskView = getTaskViewAt(childIndex);
if (taskView == null) {
- return super.getChildVisibleSize(index);
+ return super.getChildVisibleSize(childIndex);
}
- return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment(
+ return (int) (super.getChildVisibleSize(childIndex) * taskView.getSizeAdjustment(
showAsFullscreen()));
}
@@ -6341,7 +6350,7 @@
* Returns how many pixels the page is offset on the currently laid out dominant axis.
*/
private int getUnclampedScrollOffset(int pageIndex) {
- if (pageIndex == -1) {
+ if (pageIndex == INVALID_PAGE) {
return 0;
}
// Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
@@ -6869,6 +6878,11 @@
}
}
+ @Override
+ public void onCanCreateDesksChanged(boolean canCreateDesks) {
+ // TODO: b/389209338 - update the AddDesktopButton's visibility on this.
+ }
+
/** Get the color used for foreground scrimming the RecentsView for sharing. */
public static int getForegroundScrimDimColor(Context context) {
return context.getColor(R.color.overview_foreground_scrim_color);
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index 94e8c03..8df7430 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -17,6 +17,7 @@
package com.android.quickstep.views
import android.graphics.Rect
+import android.util.FloatProperty
import android.view.View
import androidx.core.view.children
import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
@@ -73,7 +74,7 @@
}
/** Counts [TaskView]s that are [DesktopTaskView] instances. */
- fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
+ private fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
/** Counts [TaskView]s that are not [DesktopTaskView] instances. */
fun getNonDesktopTaskViewCount(): Int = taskViews.count { it !is DesktopTaskView }
@@ -91,7 +92,7 @@
}
/** Returns the expected focus task. */
- fun getExpectedFocusedTask(): TaskView? =
+ fun getFirstNonDesktopTaskView(): TaskView? =
if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
else taskViews.firstOrNull()
@@ -294,7 +295,24 @@
}
}
+ var deskExplodeProgress: Float = 0f
+ set(value) {
+ field = value
+ taskViews.filterIsInstance<DesktopTaskView>().forEach { it.explodeProgress = field }
+ }
+
companion object {
+ @JvmField
+ val DESK_EXPLODE_PROGRESS =
+ object : FloatProperty<RecentsView<*, *>>("deskExplodeProgress") {
+ override fun setValue(recentsView: RecentsView<*, *>, value: Float) {
+ recentsView.mUtils.deskExplodeProgress = value
+ }
+
+ override fun get(recentsView: RecentsView<*, *>) =
+ recentsView.mUtils.deskExplodeProgress
+ }
+
val TEMP_RECT = Rect()
}
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 49ec31d..ba54232 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -789,11 +789,11 @@
private fun updateThumbnailValidity(container: TaskContainer) {
container.isThumbnailValid =
- viewModel!!.isThumbnailValid(
+ viewModel?.isThumbnailValid(
thumbnail = container.thumbnailData,
width = container.thumbnailView.width,
height = container.thumbnailView.height,
- )
+ ) ?: return
applyThumbnailSplashAlpha()
}
@@ -810,7 +810,8 @@
*/
private fun updateThumbnailMatrix(container: TaskContainer, width: Int, height: Int) {
val thumbnailPosition =
- viewModel!!.getThumbnailPosition(container.thumbnailData, width, height, isLayoutRtl)
+ viewModel?.getThumbnailPosition(container.thumbnailData, width, height, isLayoutRtl)
+ ?: return
container.updateThumbnailMatrix(thumbnailPosition.matrix)
}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
index b9d2fed..80b2c16 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
@@ -16,10 +16,19 @@
package com.android.quickstep.task.thumbnail
import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.drawable.BitmapDrawable
import android.view.LayoutInflater
+import android.view.Surface.ROTATION_0
+import androidx.core.graphics.set
import com.android.launcher3.R
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
import org.junit.Rule
@@ -81,6 +90,96 @@
}
@Test
+ fun taskThumbnailView_liveTile_withoutHeader() {
+ screenshotRule.screenshotTest("taskThumbnailView_liveTile") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ setState(TaskThumbnailUiState.LiveTile.WithoutHeader)
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_image_withoutHeader() {
+ screenshotRule.screenshotTest("taskThumbnailView_image") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY),
+ null,
+ )
+ )
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_image_withoutHeader_withImageMatrix() {
+ screenshotRule.screenshotTest("taskThumbnailView_image_withMatrix") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(
+ createBitmap(
+ width = VIEW_ENV_WIDTH / 2,
+ height = lessThanHeightMatchingAspectRatio,
+ ),
+ ROTATION_0,
+ Color.DKGRAY,
+ ),
+ null,
+ )
+ )
+ setImageMatrix(Matrix().apply { postScale(2f, 2f) })
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_splash_withoutHeader() {
+ screenshotRule.screenshotTest("taskThumbnailView_partial_splash") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY),
+ BitmapDrawable(activity.resources, createSplash()),
+ )
+ )
+ updateSplashAlpha(0.5f)
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_splash_withoutHeader_withImageMatrix() {
+ screenshotRule.screenshotTest("taskThumbnailView_partial_splash_withMatrix") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(
+ createBitmap(
+ width = VIEW_ENV_WIDTH / 2,
+ height = lessThanHeightMatchingAspectRatio,
+ ),
+ ROTATION_0,
+ Color.DKGRAY,
+ ),
+ BitmapDrawable(activity.resources, createSplash()),
+ )
+ )
+ setImageMatrix(Matrix().apply { postScale(2f, 2f) })
+ updateSplashAlpha(0.5f)
+ }
+ }
+ }
+
+ @Test
fun taskThumbnailView_dimmed_tintAmount() {
screenshotRule.screenshotTest("taskThumbnailView_dimmed_40") { activity ->
activity.actionBar?.hide()
@@ -114,6 +213,18 @@
}
}
+ @Test
+ fun taskThumbnailView_scaled_roundRoundedCorners() {
+ screenshotRule.screenshotTest("taskThumbnailView_scaledRoundedCorners") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ scaleX = 0.75f
+ scaleY = 0.3f
+ setState(BackgroundOnly(Color.YELLOW))
+ }
+ }
+ }
+
private fun createTaskThumbnailView(context: Context): TaskThumbnailView {
val taskThumbnailView =
LayoutInflater.from(context).inflate(R.layout.task_thumbnail, null, false)
@@ -122,6 +233,27 @@
return taskThumbnailView
}
+ private fun createSplash() = createBitmap(width = 20, height = 20, rectColorRotation = 1)
+
+ private fun createBitmap(
+ width: Int = VIEW_ENV_WIDTH,
+ height: Int = VIEW_ENV_HEIGHT,
+ rectColorRotation: Int = 0,
+ ) =
+ Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
+ Canvas(this).apply {
+ val paint = Paint()
+ paint.color = BITMAP_RECT_COLORS[rectColorRotation % 4]
+ drawRect(0f, 0f, width / 2f, height / 2f, paint)
+ paint.color = BITMAP_RECT_COLORS[(1 + rectColorRotation) % 4]
+ drawRect(width / 2f, 0f, width.toFloat(), height / 2f, paint)
+ paint.color = BITMAP_RECT_COLORS[(2 + rectColorRotation) % 4]
+ drawRect(0f, height / 2f, width / 2f, height.toFloat(), paint)
+ paint.color = BITMAP_RECT_COLORS[(3 + rectColorRotation) % 4]
+ drawRect(width / 2f, height / 2f, width.toFloat(), height.toFloat(), paint)
+ }
+ }
+
companion object {
@Parameters(name = "{0}")
@JvmStatic
@@ -133,5 +265,8 @@
)
const val CORNER_RADIUS = 56f
+ val BITMAP_RECT_COLORS = listOf(Color.GREEN, Color.RED, Color.BLUE, Color.CYAN)
+ const val VIEW_ENV_WIDTH = 1440
+ const val VIEW_ENV_HEIGHT = 3120
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt
similarity index 90%
rename from quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt
index 9ca9fe4..7ebef45 100644
--- a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt
@@ -27,12 +27,10 @@
import android.window.TransitionFilter
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.quickstep.SystemUiProxy
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX
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
@@ -43,7 +41,6 @@
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
-import org.mockito.quality.Strictness
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -51,12 +48,6 @@
@get:Rule val mSetFlagsRule = SetFlagsRule()
- private val mockitoSession =
- mockitoSession()
- .strictness(Strictness.LENIENT)
- .mockStatic(DesktopModeStatus::class.java)
- .startMocking()
-
private val context = mock<Context>()
private val systemUiProxy = mock<SystemUiProxy>()
private lateinit var transitionManager: DesktopAppLaunchTransitionManager
@@ -68,11 +59,6 @@
transitionManager = DesktopAppLaunchTransitionManager(context, systemUiProxy)
}
- @After
- fun tearDown() {
- mockitoSession.finishMocking()
- }
-
@Test
@EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
fun registerTransitions_appLaunchFlagEnabled_registersTransition() {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 46b5659..a456fb9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -60,6 +60,7 @@
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -573,6 +574,71 @@
}
@Test
+ fun animateToInitialState_whileDragging_inApp() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+ whenever(bubbleStashController.bubbleBarTranslationY)
+ .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+ val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+ var notifiedBubbleBarVisible = false
+ val onBubbleBarVisible = Runnable { notifiedBubbleBarVisible = true }
+ val animator =
+ BubbleBarViewAnimator(
+ bubbleBarView,
+ bubbleStashController,
+ flyoutController,
+ bubbleBarParentViewController,
+ onExpanded = emptyRunnable,
+ onBubbleBarVisible = onBubbleBarVisible,
+ animatorScheduler,
+ )
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleBarView.visibility = INVISIBLE
+ animator.animateToInitialState(
+ bubble,
+ isInApp = true,
+ isExpanding = false,
+ isDragging = true,
+ )
+ }
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ barAnimator.assertIsNotRunning()
+ assertThat(animator.isAnimating).isTrue()
+ assertThat(bubbleBarView.alpha).isEqualTo(1)
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
+ waitForFlyoutToShow()
+
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ waitForFlyoutToHide()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
+ assertThat(animator.isAnimating).isFalse()
+ assertThat(bubbleBarView.alpha).isEqualTo(1)
+ assertThat(handle.translationY).isEqualTo(0)
+ assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+ assertThat(notifiedBubbleBarVisible).isTrue()
+
+ verify(bubbleStashController, never()).stashBubbleBarImmediate()
+ }
+
+ @Test
fun animateToInitialState_inApp_autoExpanding() {
setUpBubbleBar()
setUpBubbleStashController()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
index 7776351..de6920b 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -24,6 +24,8 @@
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_NAVBAR;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.quickstep.DeviceConfigWrapper.DEFAULT_LPNH_TIMEOUT_MS;
@@ -33,9 +35,11 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.os.SystemClock;
@@ -47,6 +51,7 @@
import com.android.launcher3.dagger.LauncherAppComponent;
import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.util.AllModulesForTest;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
@@ -83,6 +88,7 @@
private NavHandleLongPressInputConsumer mUnderTest;
private SandboxContext mContext;
private float mScreenWidth;
+ private long mDownTimeMs;
@Mock InputConsumer mDelegate;
@Mock InputMonitorCompat mInputMonitor;
@Mock RecentsAnimationDeviceState mDeviceState;
@@ -91,6 +97,9 @@
@Mock NavHandleLongPressHandler mNavHandleLongPressHandler;
@Mock TopTaskTracker mTopTaskTracker;
@Mock TopTaskTracker.CachedTaskInfo mTaskInfo;
+ @Mock StatsLogManager mStatsLogManager;
+ @Mock StatsLogManager.StatsLogger mStatsLogger;
+ @Mock StatsLogManager.StatsLatencyLogger mStatsLatencyLogger;
@Before
public void setup() {
@@ -100,6 +109,11 @@
when(mDelegate.allowInterceptByParent()).thenReturn(true);
mLongPressTriggered.set(false);
when(mNavHandleLongPressHandler.getLongPressRunnable(any())).thenReturn(mLongPressRunnable);
+ when(mStatsLogger.withPackageName(any())).thenReturn(mStatsLogger);
+ when(mStatsLatencyLogger.withInstanceId(any())).thenReturn(mStatsLatencyLogger);
+ when(mStatsLatencyLogger.withLatency(anyLong())).thenReturn(mStatsLatencyLogger);
+ when(mStatsLogManager.logger()).thenReturn(mStatsLogger);
+ when(mStatsLogManager.latencyLogger()).thenReturn(mStatsLatencyLogger);
initializeObjectUnderTest();
}
@@ -124,17 +138,24 @@
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
verify(mNavHandleLongPressHandler, never()).onTouchStarted(any());
verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+ verifyNoMoreInteractions(mStatsLogManager);
+ verifyNoMoreInteractions(mStatsLogger);
+ verifyNoMoreInteractions(mStatsLatencyLogger);
}
@Test
public void testDelegateDisallowsTouchInterceptAfterTouchDown() {
+ // Touch down and wait the minimum abandonment time.
mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+ SystemClock.sleep(NavHandleLongPressInputConsumer.MIN_TIME_TO_LOG_ABANDON_MS);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
// Delegate should still get touches unless long press is triggered.
verify(mDelegate).onMotionEvent(any());
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+ // Child delegate blocks us from intercepting further motion events.
when(mDelegate.allowInterceptByParent()).thenReturn(false);
mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_MOVE));
@@ -144,6 +165,9 @@
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+ verifyNoMoreInteractions(mStatsLogger);
+ // Because we handled touch down before the child blocked additional events, log abandon.
+ verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
}
@Test
@@ -156,6 +180,12 @@
assertTrue(mLongPressTriggered.get());
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+ verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+ verifyNoMoreInteractions(mStatsLatencyLogger);
+
+ // Ensure abandon latency is still not logged after long press.
+ mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP));
+ verifyNoMoreInteractions(mStatsLatencyLogger);
}
@Test
@@ -170,6 +200,8 @@
assertTrue(mLongPressTriggered.get());
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+ verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+ verifyNoMoreInteractions(mStatsLatencyLogger);
}
@Test
@@ -184,6 +216,8 @@
assertTrue(mLongPressTriggered.get());
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+ verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+ verifyNoMoreInteractions(mStatsLatencyLogger);
}
@Test
@@ -215,6 +249,8 @@
assertTrue(mLongPressTriggered.get());
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+ verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+ verifyNoMoreInteractions(mStatsLatencyLogger);
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -235,6 +271,8 @@
assertTrue(mLongPressTriggered.get());
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+ verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+ verifyNoMoreInteractions(mStatsLatencyLogger);
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -258,6 +296,8 @@
assertFalse(mLongPressTriggered.get());
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+ verifyNoMoreInteractions(mStatsLogger);
+ verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
}
@Test
@@ -278,6 +318,8 @@
assertFalse(mLongPressTriggered.get());
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+ verifyNoMoreInteractions(mStatsLogger);
+ verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
}
@Test
@@ -299,6 +341,8 @@
assertFalse(mLongPressTriggered.get());
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+ verifyNoMoreInteractions(mStatsLogger);
+ verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
}
@Test
@@ -320,6 +364,8 @@
assertFalse(mLongPressTriggered.get());
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+ verifyNoMoreInteractions(mStatsLogger);
+ verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
}
@Test
@@ -354,6 +400,8 @@
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
// Touch cancelled.
verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+ verifyNoMoreInteractions(mStatsLogger);
+ verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -391,6 +439,8 @@
verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
// Touch cancelled.
verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+ verifyNoMoreInteractions(mStatsLogger);
+ verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -408,6 +458,9 @@
assertFalse(mLongPressTriggered.get());
verify(mNavHandleLongPressHandler, never()).onTouchStarted(any());
verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+ verifyNoMoreInteractions(mStatsLogManager);
+ verifyNoMoreInteractions(mStatsLogger);
+ verifyNoMoreInteractions(mStatsLatencyLogger);
}
@Test
@@ -422,6 +475,21 @@
mUnderTest.onHoverEvent(generateCenteredMotionEvent(ACTION_HOVER_ENTER));
verify(mDelegate, times(2)).onHoverEvent(any());
+
+ verifyNoMoreInteractions(mStatsLogManager);
+ verifyNoMoreInteractions(mStatsLogger);
+ verifyNoMoreInteractions(mStatsLatencyLogger);
+ }
+
+ @Test
+ public void testNoLogsForShortTouch() {
+ mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+ SystemClock.sleep(10);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP));
+ verifyNoMoreInteractions(mStatsLogManager);
+ verifyNoMoreInteractions(mStatsLogger);
+ verifyNoMoreInteractions(mStatsLatencyLogger);
}
private void initializeObjectUnderTest() {
@@ -437,6 +505,8 @@
mUnderTest = new NavHandleLongPressInputConsumer(mContext, mDelegate, mInputMonitor,
mDeviceState, mNavHandle, mGestureState);
mUnderTest.setNavHandleLongPressHandler(mNavHandleLongPressHandler);
+ mUnderTest.setStatsLogManager(mStatsLogManager);
+ mDownTimeMs = 0;
}
/** Generate a motion event centered horizontally in the screen. */
@@ -449,8 +519,12 @@
return generateMotionEvent(motionAction, mScreenWidth / 2f, y);
}
- private static MotionEvent generateMotionEvent(int motionAction, float x, float y) {
- return MotionEvent.obtain(0, 0, motionAction, x, y, 0);
+ private MotionEvent generateMotionEvent(int motionAction, float x, float y) {
+ if (motionAction == ACTION_DOWN) {
+ mDownTimeMs = SystemClock.uptimeMillis();
+ }
+ long eventTime = SystemClock.uptimeMillis();
+ return MotionEvent.obtain(mDownTimeMs, eventTime, motionAction, x, y, 0);
}
private static AutoCloseable overrideTwoStageFlag(boolean value) {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
index e10afc4..40d5e02 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
@@ -22,7 +22,6 @@
import com.android.systemui.shared.recents.model.ThumbnailData
import kotlinx.coroutines.yield
import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
class FakeTaskThumbnailDataSource : TaskThumbnailDataSource {
@@ -31,6 +30,8 @@
private val completionPrevented: MutableSet<Int> = mutableSetOf()
private val getThumbnailCalls = mutableMapOf<Int, Int>()
+ var highResEnabled = true
+
/** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */
override suspend fun getThumbnail(task: Task): ThumbnailData {
getThumbnailCalls[task.key.id] = (getThumbnailCalls[task.key.id] ?: 0) + 1
@@ -38,9 +39,10 @@
while (task.key.id in completionPrevented) {
yield()
}
- return mock<ThumbnailData>().also {
- whenever(it.thumbnail).thenReturn(taskIdToBitmap[task.key.id])
- }
+ return ThumbnailData(
+ thumbnail = taskIdToBitmap[task.key.id],
+ reducedResolution = !highResEnabled,
+ )
}
fun getNumberOfGetThumbnailCalls(taskId: Int): Int = getThumbnailCalls[taskId] ?: 0
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
index 41f6bfd..b91f8bd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
@@ -19,16 +19,19 @@
import android.content.ComponentName
import android.content.Intent
import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
import com.android.systemui.shared.recents.model.Task.TaskKey
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
+@RunWith(AndroidJUnit4::class)
class TaskVisualsChangedDelegateTest {
private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier()
private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier()
@@ -83,21 +86,21 @@
// Correct match
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
- expectedListener
+ expectedListener,
)
// 1 out of 2 match
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 2, pkg = PACKAGE_NAME, userId = 1),
- listener
+ listener,
)
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 3, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 2),
- listener
+ listener,
)
// 0 out of 2 match
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 4, pkg = PACKAGE_NAME, userId = 2),
- listener
+ listener,
)
systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
@@ -112,11 +115,11 @@
val newListener = mock<TaskIconChangedCallback>()
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
- replacedListener
+ replacedListener,
)
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
- newListener
+ newListener,
)
systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
@@ -132,11 +135,11 @@
val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
systemUnderTest.registerTaskThumbnailChangedCallback(
createTaskKey(id = 1),
- expectedListener
+ expectedListener,
)
systemUnderTest.registerTaskThumbnailChangedCallback(
createTaskKey(id = 2),
- additionalListener
+ additionalListener,
)
systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData)
@@ -146,22 +149,41 @@
}
@Test
- fun onHighResLoadingStateChanged_notifiesAllListeners() {
+ fun onHighResLoadingStateChanged_toEnabled_notifiesAllListeners() {
val expectedListener = mock<TaskThumbnailChangedCallback>()
val additionalListener = mock<TaskThumbnailChangedCallback>()
systemUnderTest.registerTaskThumbnailChangedCallback(
createTaskKey(id = 1),
- expectedListener
+ expectedListener,
)
systemUnderTest.registerTaskThumbnailChangedCallback(
createTaskKey(id = 2),
- additionalListener
+ additionalListener,
)
systemUnderTest.onHighResLoadingStateChanged(true)
- verify(expectedListener).onHighResLoadingStateChanged()
- verify(additionalListener).onHighResLoadingStateChanged()
+ verify(expectedListener).onHighResLoadingStateChanged(true)
+ verify(additionalListener).onHighResLoadingStateChanged(true)
+ }
+
+ @Test
+ fun onHighResLoadingStateChanged_toDisabled_notifiesAllListeners() {
+ val expectedListener = mock<TaskThumbnailChangedCallback>()
+ val additionalListener = mock<TaskThumbnailChangedCallback>()
+ systemUnderTest.registerTaskThumbnailChangedCallback(
+ createTaskKey(id = 1),
+ expectedListener,
+ )
+ systemUnderTest.registerTaskThumbnailChangedCallback(
+ createTaskKey(id = 2),
+ additionalListener,
+ )
+
+ systemUnderTest.onHighResLoadingStateChanged(false)
+
+ verify(expectedListener).onHighResLoadingStateChanged(false)
+ verify(additionalListener).onHighResLoadingStateChanged(false)
}
@Test
@@ -171,7 +193,7 @@
val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
systemUnderTest.registerTaskThumbnailChangedCallback(
createTaskKey(id = 1),
- replacedListener1
+ replacedListener1,
)
systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), newListener1)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
index 823f808..10be6fd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
@@ -326,8 +326,9 @@
}
@Test
- fun onHighResLoadingStateChanged_setsNewThumbnailDataOnTask() =
+ fun onHighResLoadingStateChanged_highResReplacesLowResThumbnail() =
testScope.runTest {
+ taskThumbnailDataSource.highResEnabled = false
recentsModel.seedTasks(defaultTaskList)
systemUnderTest.getAllTaskData(forceRefresh = true)
@@ -337,16 +338,77 @@
val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
val taskDataFlow = systemUnderTest.getTaskDataById(1)
- val task1ThumbnailValues = mutableListOf<Bitmap?>()
+ val task1ThumbnailValues = mutableListOf<ThumbnailData?>()
testScope.backgroundScope.launch {
- taskDataFlow.map { it?.thumbnail?.thumbnail }.toList(task1ThumbnailValues)
+ taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues)
}
taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
+ taskThumbnailDataSource.highResEnabled = true
taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
- assertThat(task1ThumbnailValues.first()).isEqualTo(expectedPreviousBitmap)
- assertThat(task1ThumbnailValues.last()).isEqualTo(expectedBitmap)
+ val firstThumbnailValue = task1ThumbnailValues.first()!!
+ assertThat(firstThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap)
+ assertThat(firstThumbnailValue.reducedResolution).isTrue()
+
+ val lastThumbnailValue = task1ThumbnailValues.last()!!
+ assertThat(lastThumbnailValue.thumbnail).isEqualTo(expectedBitmap)
+ assertThat(lastThumbnailValue.reducedResolution).isFalse()
+ }
+
+ @Test
+ fun onHighResLoadingStateChanged_invisibleTaskIgnored() =
+ testScope.runTest {
+ taskThumbnailDataSource.highResEnabled = false
+ recentsModel.seedTasks(defaultTaskList)
+ systemUnderTest.getAllTaskData(forceRefresh = true)
+
+ systemUnderTest.setVisibleTasks(setOf(1))
+
+ val invisibleTaskId = 2
+ val taskDataFlow = systemUnderTest.getTaskDataById(invisibleTaskId)
+
+ val task2ThumbnailValues = mutableListOf<ThumbnailData?>()
+ testScope.backgroundScope.launch {
+ taskDataFlow.map { it?.thumbnail }.toList(task2ThumbnailValues)
+ }
+
+ taskThumbnailDataSource.highResEnabled = true
+ taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
+
+ assertThat(task2ThumbnailValues.filterNotNull()).isEmpty()
+ assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(2)).isEqualTo(0)
+ }
+
+ @Test
+ fun onHighResLoadingStateChanged_lowResDoesNotReplaceHighResThumbnail() =
+ testScope.runTest {
+ taskThumbnailDataSource.highResEnabled = true
+ recentsModel.seedTasks(defaultTaskList)
+ systemUnderTest.getAllTaskData(forceRefresh = true)
+
+ systemUnderTest.setVisibleTasks(setOf(1))
+
+ val expectedBitmap = mock<Bitmap>()
+ val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
+ val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+ val task1ThumbnailValues = mutableListOf<ThumbnailData?>()
+ testScope.backgroundScope.launch {
+ taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues)
+ }
+
+ taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
+ taskThumbnailDataSource.highResEnabled = false
+ taskVisualsChangedDelegate.onHighResLoadingStateChanged(false)
+
+ val firstThumbnailValue = task1ThumbnailValues.first()!!
+ assertThat(firstThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap)
+ assertThat(firstThumbnailValue.reducedResolution).isFalse()
+
+ val lastThumbnailValue = task1ThumbnailValues.last()!!
+ assertThat(lastThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap)
+ assertThat(lastThumbnailValue.reducedResolution).isFalse()
}
@Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
index ee70e0a..76d36d3 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
@@ -16,7 +16,9 @@
package com.android.quickstep.util
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.Context
+import android.content.res.Resources
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.launcher3.apppairs.AppPairIcon
import com.android.launcher3.logging.StatsLogManager
@@ -28,6 +30,7 @@
import com.android.quickstep.TopTaskTracker.CachedTaskInfo
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66
import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33
@@ -54,6 +57,7 @@
@RunWith(AndroidJUnit4::class)
class AppPairsControllerTest {
@Mock lateinit var context: Context
+ @Mock lateinit var resources: Resources
@Mock lateinit var splitSelectStateController: SplitSelectStateController
@Mock lateinit var statsLogManager: StatsLogManager
@@ -109,6 +113,8 @@
doNothing()
.whenever(spyAppPairsController)
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+ whenever(mockAppPairIcon.context.resources).thenReturn(resources)
+ whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(false)
}
@Test
@@ -392,6 +398,68 @@
}
@Test
+ fun handleAppPairLaunchInApp_freeformTask1IsOnScreen_shouldLaunchAppPair() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true)
+ /// Test launching apps 1 and 2 from app pair
+ whenever(mockTaskKey1.getId()).thenReturn(1)
+ whenever(mockTaskKey2.getId()).thenReturn(2)
+ // Task 1 is in freeform windowing mode
+ mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM
+ // ... and app 1 is already on screen
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(1))).thenReturn(true)
+ } else {
+ whenever(mockCachedTaskInfo.taskId).thenReturn(1)
+ }
+
+ // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+ spyAppPairsController.handleAppPairLaunchInApp(
+ mockAppPairIcon,
+ listOf(mockItemInfo1, mockItemInfo2),
+ )
+ verify(splitSelectStateController)
+ .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+ val callback: Consumer<Array<Task>> = callbackCaptor.value
+ callback.accept(arrayOf(mockTask1, mockTask2))
+
+ // Verify that launchAppPair was called
+ verify(spyAppPairsController, times(1)).launchAppPair(any(), any())
+ verify(spyAppPairsController, never())
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+ }
+
+ @Test
+ fun handleAppPairLaunchInApp_freeformTask2IsOnScreen_shouldLaunchAppPair() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true)
+ /// Test launching apps 1 and 2 from app pair
+ whenever(mockTaskKey1.getId()).thenReturn(1)
+ whenever(mockTaskKey2.getId()).thenReturn(2)
+ // Task 2 is in freeform windowing mode
+ mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM
+ // ... and app 2 is already on screen
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(2))).thenReturn(true)
+ } else {
+ whenever(mockCachedTaskInfo.taskId).thenReturn(2)
+ }
+
+ // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+ spyAppPairsController.handleAppPairLaunchInApp(
+ mockAppPairIcon,
+ listOf(mockItemInfo1, mockItemInfo2),
+ )
+ verify(splitSelectStateController)
+ .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+ val callback: Consumer<Array<Task>> = callbackCaptor.value
+ callback.accept(arrayOf(mockTask1, mockTask2))
+
+ // Verify that launchAppPair was called
+ verify(spyAppPairsController, times(1)).launchAppPair(any(), any())
+ verify(spyAppPairsController, never())
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+ }
+
+ @Test
fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedSingleAppIsFullscreen() {
// Test launching apps 1 and 2 from app pair
whenever(mockTaskKey1.getId()).thenReturn(1)
diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
index fa2eb1e..d52d054 100644
--- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
+++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -91,6 +91,7 @@
private AppWidgetProviderInfo mApp4Provider1;
private AppWidgetProviderInfo mApp4Provider2;
private AppWidgetProviderInfo mApp5Provider1;
+ private AppWidgetProviderInfo mApp6PinOnlyProvider1;
private List<AppWidgetProviderInfo> allWidgets;
private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback();
@@ -117,8 +118,14 @@
ComponentName.createRelative("app4", ".provider2"));
mApp5Provider1 = createAppWidgetProviderInfo(
ComponentName.createRelative("app5", "provider1"));
+ mApp6PinOnlyProvider1 = createAppWidgetProviderInfo(
+ ComponentName.createRelative("app6", "provider1"),
+ /*hideFromPicker=*/ true
+ );
+
+
allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1,
- mApp4Provider1, mApp4Provider2, mApp5Provider1);
+ mApp4Provider1, mApp4Provider2, mApp5Provider1, mApp6PinOnlyProvider1);
mLauncherApps = mModelHelper.sandboxContext.spyService(LauncherApps.class);
doAnswer(i -> {
@@ -270,6 +277,32 @@
});
}
+ @Test
+ public void widgetsRecommendations_excludesWidgetsHiddenForPicker() {
+ runOnExecutorSync(MODEL_EXECUTOR, () -> {
+
+ // Not installed widget - hence eligible
+ AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1",
+ mUserHandle);
+ // Provider marked as hidden from picker - hence not eligible
+ AppTarget widget6 = new AppTarget(new AppTargetId("app6"), "app6", "provider1",
+ mUserHandle);
+
+ mCallback.mRecommendedWidgets = null;
+ mModelHelper.getModel().enqueueModelUpdateTask(
+ newWidgetsPredicationTask(List.of(widget1, widget6)));
+ runOnExecutorSync(MAIN_EXECUTOR, () -> { });
+
+ // Only widget 1 (and no widget 6 as its meant to be hidden from picker).
+ List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
+ .stream()
+ .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
+ .collect(Collectors.toList());
+ assertThat(recommendedWidgets).hasSize(1);
+ assertThat(recommendedWidgets.get(0).componentName.getPackageName()).isEqualTo("app1");
+ });
+ }
+
private void assertWidgetInfo(
LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) {
assertThat(actual.provider).isEqualTo(expected.provider);
diff --git a/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt b/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt
new file mode 100644
index 0000000..4b8f2a2
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2025 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.statehandlers
+
+import android.content.Context
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.quickstep.SystemUiProxy
+import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND
+import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+/**
+ * Tests the behavior of [DesktopVisibilityController] in regards to multiple desktops and multiple
+ * displays.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DesktopVisibilityControllerTest {
+
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val mockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .mockStatic(DesktopModeStatus::class.java)
+ .startMocking()
+
+ private val context = mock<Context>()
+ private val systemUiProxy = mock<SystemUiProxy>()
+ private val lifeCycleTracker = mock<DaggerSingletonTracker>()
+ private lateinit var desktopVisibilityController: DesktopVisibilityController
+
+ @Before
+ fun setUp() {
+ whenever(context.resources).thenReturn(mock())
+ whenever(DesktopModeStatus.enableMultipleDesktops(context)).thenReturn(true)
+ desktopVisibilityController =
+ DesktopVisibilityController(context, systemUiProxy, lifeCycleTracker)
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND)
+ fun noCrashWhenCheckingNonExistentDisplay() {
+ assertFalse(desktopVisibilityController.isInDesktopMode(displayId = 500))
+ assertFalse(desktopVisibilityController.isInDesktopModeAndNotInOverview(displayId = 300))
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index 76aab39..a523e02 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -20,6 +20,7 @@
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.view.Display.DEFAULT_DISPLAY
import androidx.test.platform.app.InstrumentationRegistry
@@ -106,6 +107,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+ @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION)
fun createDesktopTaskShortcutFactory_transparentTask() {
val baseComponent = ComponentName("", /* class */ "")
val taskKey =
diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
index 818841a..2db94f6 100644
--- a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
@@ -20,6 +20,7 @@
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.view.Display.DEFAULT_DISPLAY
@@ -113,6 +114,7 @@
Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
)
+ @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION)
fun createExternalDisplayTaskShortcut_transparentTask() {
val baseComponent = ComponentName("", /* class */ "")
val taskKey =
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index aaaa0e4..1c87bce 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
@@ -50,6 +51,7 @@
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
import com.android.launcher3.util.TestUtil;
import com.android.launcher3.util.Wait;
+import com.android.launcher3.util.rule.ScreenRecordRule;
import com.android.launcher3.util.rule.TestStabilityRule;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
@@ -542,6 +544,7 @@
@Test
@PortraitLandscape
+ @ScreenRecordRule.ScreenRecord // TODO(b/396447643): Remove screen record.
public void testDismissCancel() throws Exception {
startTestAppsWithCheck();
Overview overview = mLauncher.goHome().switchToOverview();
@@ -579,6 +582,39 @@
numTasks - 1, recentsView.getTaskViewCount()));
}
+ @Test
+ @PortraitLandscape
+ public void testDismissLastGridRow() throws Exception {
+ assumeTrue(mLauncher.isTablet());
+ mLauncher.goHome().switchToOverview().dismissAllTasks();
+ startTestAppsWithCheck();
+ startTestActivity(3);
+ startTestActivity(4);
+ runOnRecentsView(
+ recentsView -> assertNotEquals("Grid overview should have unequal row counts",
+ recentsView.getTopRowTaskCountForTablet(),
+ recentsView.getBottomRowTaskCountForTablet()));
+ Overview overview = mLauncher.goHome().switchToOverview();
+ assertIsInState("Launcher internal state didn't switch to Overview",
+ ExpectedState.OVERVIEW);
+ overview.flingForwardUntilClearAllVisible();
+ assertTrue("Clear All not visible.", overview.isClearAllVisible());
+ final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount);
+ OverviewTask lastGridTask = overview.getCurrentTasksForTablet().stream().min(
+ Comparator.comparingInt(OverviewTask::getTaskCenterX)).get();
+ assertNotNull("lastGridTask null.", lastGridTask);
+
+ lastGridTask.dismiss();
+
+ runOnRecentsView(recentsView -> assertEquals(
+ "Dismissing a lastGridTask didn't remove 1 lastGridTask from Overview",
+ numTasks - 1, recentsView.getTaskViewCount()));
+ runOnRecentsView(recentsView -> assertEquals("Grid overview should have equal row counts.",
+ recentsView.getTopRowTaskCountForTablet(),
+ recentsView.getBottomRowTaskCountForTablet()));
+ assertTrue("Clear All not visible.", overview.isClearAllVisible());
+ }
+
private void startTestAppsWithCheck() throws Exception {
startTestApps();
expectLaunchedAppState();
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 03dd943..bd42b2b 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -27,6 +27,7 @@
import static com.android.launcher3.icons.BitmapInfo.FLAG_SKIP_USER_BADGE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK;
@@ -76,7 +77,6 @@
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.graphics.PreloadIconDrawable;
-import com.android.launcher3.graphics.ShapeDelegate;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
@@ -723,8 +723,7 @@
protected void drawDotIfNecessary(Canvas canvas) {
if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
getIconBounds(mDotParams.iconBounds);
- Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
- ShapeDelegate.getNormalizationScale());
+ Utilities.scaleRectAboutCenter(mDotParams.iconBounds, ICON_VISIBLE_AREA_FACTOR);
final int scrollX = getScrollX();
final int scrollY = getScrollY();
canvas.translate(scrollX, scrollY);
@@ -773,9 +772,7 @@
return;
}
getIconBounds(mRunningAppIconBounds);
- Utilities.scaleRectAboutCenter(
- mRunningAppIconBounds,
- ShapeDelegate.getNormalizationScale());
+ Utilities.scaleRectAboutCenter(mRunningAppIconBounds, ICON_VISIBLE_AREA_FACTOR);
final boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED;
final int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin;
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 4561506..c85ca49 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -55,7 +55,6 @@
import com.android.launcher3.folder.ClippedFolderIconLayoutRule;
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.DotRenderer;
-import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.responsive.CalculatedCellSpec;
import com.android.launcher3.responsive.CalculatedHotseatSpec;
@@ -1374,7 +1373,7 @@
updateHotseatSizes(iconSizePx);
// Folder icon
- folderIconSizePx = IconNormalizer.getNormalizedCircleSize(iconSizePx);
+ folderIconSizePx = Math.round(iconSizePx * ICON_VISIBLE_AREA_FACTOR);
folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2;
// Update widget padding:
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index c2d6df5..f189549 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -85,6 +85,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import javax.inject.Inject;
@@ -252,7 +253,7 @@
public Point defaultWallpaperSize;
- private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
+ private final List<OnIDPChangeListener> mChangeListeners = new CopyOnWriteArrayList<>();
@Inject
InvariantDeviceProfile(
@@ -312,7 +313,7 @@
context,
gridName,
displayInfo,
- RestoreDbTask.isPending(mPrefs),
+ (RestoreDbTask.isPending(mPrefs) && !Flags.oneGridSpecs()),
mPrefs.get(FIXED_LANDSCAPE_MODE)
);
@@ -505,6 +506,7 @@
* Updates the current grid, this triggers a new IDP, reloads the database and triggers a grid
* migration.
*/
+ @VisibleForTesting
public void setCurrentGrid(Context context, String newGridName) {
mPrefs.put(GRID_NAME, newGridName);
MAIN_EXECUTOR.execute(() -> {
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 71013c3..bf2ad92 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -16,54 +16,32 @@
package com.android.launcher3;
-import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
-import static android.content.Context.RECEIVER_EXPORTED;
-
-import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
-import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
-import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI;
-
-import android.content.ComponentName;
import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.ArchiveCompatibilityParams;
import android.util.Log;
import androidx.annotation.Nullable;
-import androidx.core.os.BuildCompat;
+import com.android.launcher3.dagger.LauncherComponentProvider;
import com.android.launcher3.graphics.ThemeManager;
-import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.IconProvider;
import com.android.launcher3.icons.LauncherIconProvider;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.model.ModelLauncherCallbacks;
+import com.android.launcher3.model.ModelInitializer;
import com.android.launcher3.model.WidgetsFilterDataProvider;
-import com.android.launcher3.notification.NotificationListener;
import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.pm.InstallSessionTracker;
import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.LockedUserState;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.SettingsCache;
-import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.launcher3.util.TraceHelper;
import com.android.launcher3.widget.custom.CustomWidgetManager;
public class LauncherAppState implements SafeCloseable {
public static final String TAG = "LauncherAppState";
- public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
// We do not need any synchronization for this variable as its only written on UI thread.
public static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
@@ -94,82 +72,21 @@
mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
() -> context.getPackageManager().isSafeMode());
- mInvariantDeviceProfile.addOnChangeListener(modelPropertiesChanged -> {
- if (modelPropertiesChanged) {
- refreshAndReloadLauncher();
- }
- });
- ThemeChangeListener themeChangeListener = this::refreshAndReloadLauncher;
- ThemeManager.INSTANCE.get(context).addChangeListener(themeChangeListener);
- mOnTerminateCallback.add(() ->
- ThemeManager.INSTANCE.get(context).removeChangeListener(themeChangeListener));
-
- ModelLauncherCallbacks callbacks = mModel.newModelCallbacks();
- LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
- launcherApps.registerCallback(callbacks);
- mOnTerminateCallback.add(() ->
- mContext.getSystemService(LauncherApps.class).unregisterCallback(callbacks));
-
- if (BuildCompat.isAtLeastV() && Flags.enableSupportForArchiving()) {
- ArchiveCompatibilityParams params = new ArchiveCompatibilityParams();
- params.setEnableUnarchivalConfirmation(false);
- params.setEnableIconOverlay(!Flags.useNewIconForArchivedApps());
- launcherApps.setArchiveCompatibility(params);
- }
-
- SimpleBroadcastReceiver modelChangeReceiver =
- new SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR, mModel::onBroadcastIntent);
- modelChangeReceiver.register(
- ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
- if (BuildConfig.IS_STUDIO_BUILD) {
- modelChangeReceiver.register(RECEIVER_EXPORTED, ACTION_FORCE_ROLOAD);
- }
- mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafely());
-
- SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
- .addUserEventListener(mModel::onUserEvent);
- mOnTerminateCallback.add(userChangeListener::close);
-
- if (enableSmartspaceRemovalToggle()) {
- OnSharedPreferenceChangeListener firstPagePinnedItemListener =
- new OnSharedPreferenceChangeListener() {
- @Override
- public void onSharedPreferenceChanged(
- SharedPreferences sharedPreferences, String key) {
- if (SMARTSPACE_ON_HOME_SCREEN.equals(key)) {
- mModel.forceReload();
- }
- }
- };
- LauncherPrefs.getPrefs(mContext).registerOnSharedPreferenceChangeListener(
- firstPagePinnedItemListener);
- mOnTerminateCallback.add(() -> LauncherPrefs.getPrefs(mContext)
- .unregisterOnSharedPreferenceChangeListener(firstPagePinnedItemListener));
- }
-
- LockedUserState.get(context).runOnUserUnlocked(() -> {
- CustomWidgetManager cwm = CustomWidgetManager.INSTANCE.get(mContext);
- mOnTerminateCallback.add(cwm.addWidgetRefreshCallback(mModel::rebindCallbacks)::close);
-
- SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener(
- mModel::onAppIconChanged, MODEL_EXECUTOR.getHandler());
- mOnTerminateCallback.add(iconChangeTracker::close);
-
- InstallSessionTracker installSessionTracker =
- InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(callbacks);
- mOnTerminateCallback.add(installSessionTracker::unregister);
- });
-
- // Register an observer to rebind the notification listener when dots are re-enabled.
- SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
- SettingsCache.OnChangeListener notificationLister = this::onNotificationSettingsChanged;
- settingsCache.register(NOTIFICATION_BADGING_URI, notificationLister);
- onNotificationSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI));
- mOnTerminateCallback.add(() ->
- settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationLister));
- // Register an observer to notify Launcher about Private Space settings toggle.
- registerPrivateSpaceHideWhenLockListener(settingsCache);
+ ModelInitializer initializer = new ModelInitializer(
+ context,
+ LauncherComponentProvider.get(context).getIconPool(),
+ mIconCache,
+ mInvariantDeviceProfile,
+ ThemeManager.INSTANCE.get(context),
+ UserCache.INSTANCE.get(context),
+ SettingsCache.INSTANCE.get(context),
+ mIconProvider,
+ CustomWidgetManager.INSTANCE.get(context),
+ InstallSessionHelper.INSTANCE.get(context),
+ closeable -> mOnTerminateCallback.add(closeable::close)
+ );
+ initializer.initialize(mModel);
}
public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
@@ -186,32 +103,6 @@
mOnTerminateCallback.add(mModel::destroy);
}
- private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
- if (areNotificationDotsEnabled) {
- NotificationListener.requestRebind(new ComponentName(
- mContext, NotificationListener.class));
- }
- }
-
- private void registerPrivateSpaceHideWhenLockListener(SettingsCache settingsCache) {
- SettingsCache.OnChangeListener psHideWhenLockChangedListener =
- this::onPrivateSpaceHideWhenLockChanged;
- settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psHideWhenLockChangedListener);
- mOnTerminateCallback.add(() -> settingsCache.unregister(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI,
- psHideWhenLockChangedListener));
- }
-
- private void onPrivateSpaceHideWhenLockChanged(boolean isPrivateSpaceHideOnLockEnabled) {
- mModel.forceReload();
- }
-
- private void refreshAndReloadLauncher() {
- LauncherIcons.clearPool(mContext);
- mIconCache.updateIconParams(
- mInvariantDeviceProfile.fillResIconDpi, mInvariantDeviceProfile.iconBitmapSize);
- mModel.forceReload();
- }
-
/**
* Call from Application.onTerminate(), which is not guaranteed to ever be called.
*/
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
index 185629b..6e4276d 100644
--- a/src/com/android/launcher3/LauncherModel.kt
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -15,13 +15,11 @@
*/
package com.android.launcher3
-import android.app.admin.DevicePolicyManager
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.os.UserHandle
import android.text.TextUtils
-import android.util.Log
import android.util.Pair
import androidx.annotation.WorkerThread
import com.android.launcher3.celllayout.CellPosMapper
@@ -47,7 +45,6 @@
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.pm.UserCache
import com.android.launcher3.shortcuts.ShortcutRequest
-import com.android.launcher3.testing.shared.TestProtocol.sDebugTracing
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.launcher3.util.Executors.MODEL_EXECUTOR
import com.android.launcher3.util.PackageManagerHelper
@@ -173,15 +170,8 @@
}
}
- fun onBroadcastIntent(intent: Intent) {
- if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=$intent")
- when (intent.action) {
- LauncherAppState.ACTION_FORCE_ROLOAD ->
- // If we have changed locale we need to clear out the labels in all apps/workspace.
- forceReload()
- DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED ->
- enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
- }
+ fun reloadStringCache() {
+ enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
}
/**
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 31d0da0..f6acda4 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -20,6 +20,7 @@
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherPrefs;
+import com.android.launcher3.graphics.GridCustomizationsProxy;
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.LauncherIcons.IconPool;
import com.android.launcher3.model.ItemInstallQueue;
@@ -75,6 +76,8 @@
InvariantDeviceProfile getIDP();
IconPool getIconPool();
+ GridCustomizationsProxy getGridCustomizationsProxy();
+
/** Builder for LauncherBaseAppComponent. */
interface Builder {
@BindsInstance Builder appContext(@ApplicationContext Context context);
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index bff323c..981b78b 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -62,7 +62,7 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.FastBitmapDrawable;
-import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.views.ActivityContext;
@@ -246,10 +246,12 @@
public void setItemInfo(final ItemInfo info) {
// Load the adaptive icon on a background thread and add the view in ui thread.
MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
+ ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
int w = mWidth;
int h = mHeight;
Pair<AdaptiveIconDrawable, Drawable> fullDrawable = Utilities.getFullDrawable(
- mActivity, info, w, h, true /* shouldThemeIcon */);
+ mActivity, info, w, h,
+ themeManager.isIconThemeEnabled());
if (fullDrawable != null) {
AdaptiveIconDrawable adaptiveIcon = fullDrawable.first;
int blurMargin = (int) mActivity.getResources()
@@ -261,12 +263,7 @@
// be scaled down due to icon normalization.
mBadge = fullDrawable.second;
FastBitmapDrawable.setBadgeBounds(mBadge, bounds);
-
- try (LauncherIcons li = LauncherIcons.obtain(mActivity)) {
- // Since we just want the scale, avoid heavy drawing operations
- Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(
- new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null)));
- }
+ Utilities.scaleRectAboutCenter(bounds, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
// Shrink very tiny bit so that the clip path is smaller than the original bitmap
// that has anti aliased edges and shadows.
@@ -274,7 +271,6 @@
Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
adaptiveIcon.setBounds(shrunkBounds);
- ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
final Path mask = (adaptiveIcon instanceof FolderAdaptiveIcon
? themeManager.getFolderShape() : themeManager.getIconShape())
.getPath(shrunkBounds);
@@ -567,7 +563,7 @@
return mContentViewParent;
}
- /** Return true if {@link mContent} is a {@link AppWidgetHostView}. */
+ /** Return true if {@link #mContent} is a {@link AppWidgetHostView}. */
public boolean containsAppWidgetHostView() {
return mContent instanceof AppWidgetHostView;
}
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java
similarity index 87%
rename from src/com/android/launcher3/graphics/GridCustomizationsProvider.java
rename to src/com/android/launcher3/graphics/GridCustomizationsProxy.java
index 12c65c7..01c9d7e 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java
@@ -22,7 +22,6 @@
import static java.util.Objects.requireNonNullElse;
-import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -45,9 +44,13 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherPrefs;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.shapes.IconShapeModel;
import com.android.launcher3.shapes.ShapesProvider;
+import com.android.launcher3.util.ContentProviderProxy.ProxyProvider;
+import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.RunnableList;
@@ -62,6 +65,8 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
+import javax.inject.Inject;
+
/**
* Exposes various launcher grid options and allows the caller to change them.
* APIs:
@@ -76,7 +81,7 @@
* rows: number of rows in the grid
* cols: number of columns in the grid
* preview_count: number of previews available for this grid option. The preview uri
- * looks like /preview/<grid-name>/<preview index starting with 0>
+ * looks like /preview/[grid-name]/[preview index starting with 0]
* is_default: true if this grid option is currently set to the system
*
* /get_preview: Open a file stream for the grid preview
@@ -85,7 +90,8 @@
* shape_key: key of the shape to apply
* name: key of the grid to apply
*/
-public class GridCustomizationsProvider extends ContentProvider {
+@LauncherAppSingleton
+public class GridCustomizationsProxy implements ProxyProvider {
private static final String TAG = "GridCustomizationsProvider";
@@ -132,17 +138,31 @@
private final Set<PreviewLifecycleObserver> mActivePreviews =
Collections.newSetFromMap(new ConcurrentHashMap<>());
- @Override
- public boolean onCreate() {
- return true;
+ private final Context mContext;
+ private final ThemeManager mThemeManager;
+ private final LauncherPrefs mPrefs;
+ private final InvariantDeviceProfile mIdp;
+
+ @Inject
+ GridCustomizationsProxy(
+ @ApplicationContext Context context,
+ ThemeManager themeManager,
+ LauncherPrefs prefs,
+ InvariantDeviceProfile idp,
+ DaggerSingletonTracker lifeCycle
+ ) {
+ mContext = context;
+ mThemeManager = themeManager;
+ mPrefs = prefs;
+ mIdp = idp;
+ lifeCycle.addCloseable(() -> mActivePreviews.forEach(PreviewLifecycleObserver::binderDied));
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
- Context context = getContext();
String path = uri.getPath();
- if (context == null || path == null) {
+ if (path == null) {
return null;
}
@@ -151,8 +171,7 @@
if (Flags.newCustomizationPickerUi()) {
MatrixCursor cursor = new MatrixCursor(new String[]{
KEY_SHAPE_KEY, KEY_SHAPE_TITLE, KEY_PATH, KEY_IS_DEFAULT});
- String currentShapePath =
- ThemeManager.INSTANCE.get(context).getIconState().getIconMask();
+ String currentShapePath = mThemeManager.getIconState().getIconMask();
Optional<IconShapeModel> selectedShape = ShapesProvider.INSTANCE.getIconShapes()
.values()
.stream()
@@ -180,8 +199,7 @@
MatrixCursor cursor = new MatrixCursor(new String[]{
KEY_NAME, KEY_GRID_TITLE, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT,
KEY_IS_DEFAULT, KEY_GRID_ICON_ID});
- InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext());
- List<GridOption> gridOptionList = idp.parseAllGridOptions(getContext());
+ List<GridOption> gridOptionList = mIdp.parseAllGridOptions(mContext);
if (com.android.launcher3.Flags.oneGridSpecs()) {
gridOptionList.sort(Comparator
.comparingInt((GridOption option) -> option.numColumns)
@@ -194,8 +212,8 @@
.add(KEY_ROWS, gridOption.numRows)
.add(KEY_COLS, gridOption.numColumns)
.add(KEY_PREVIEW_COUNT, 1)
- .add(KEY_IS_DEFAULT, idp.numColumns == gridOption.numColumns
- && idp.numRows == gridOption.numRows)
+ .add(KEY_IS_DEFAULT, mIdp.numColumns == gridOption.numColumns
+ && mIdp.numRows == gridOption.numRows)
.add(KEY_GRID_ICON_ID, gridOption.gridIconId);
}
return cursor;
@@ -203,8 +221,7 @@
case GET_ICON_THEMED:
case ICON_THEMED: {
MatrixCursor cursor = new MatrixCursor(new String[]{BOOLEAN_VALUE});
- cursor.newRow().add(BOOLEAN_VALUE,
- ThemeManager.INSTANCE.get(getContext()).isMonoThemeEnabled() ? 1 : 0);
+ cursor.newRow().add(BOOLEAN_VALUE, mThemeManager.isMonoThemeEnabled() ? 1 : 0);
return cursor;
}
default:
@@ -213,38 +230,21 @@
}
@Override
- public String getType(Uri uri) {
- return "vnd.android.cursor.dir/launcher_grid";
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues initialValues) {
- return null;
- }
-
- @Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- return 0;
- }
-
- @Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
String path = uri.getPath();
- Context context = getContext();
- if (path == null || context == null) {
+ if (path == null) {
return 0;
}
switch (path) {
case KEY_DEFAULT_GRID: {
if (Flags.newCustomizationPickerUi()) {
- LauncherPrefs.INSTANCE.get(context).put(PREF_ICON_SHAPE,
+ mPrefs.put(PREF_ICON_SHAPE,
requireNonNullElse(values.getAsString(KEY_SHAPE_KEY), ""));
}
String gridName = values.getAsString(KEY_NAME);
- InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
// Verify that this is a valid grid option
GridOption match = null;
- for (GridOption option : idp.parseAllGridOptions(context)) {
+ for (GridOption option : mIdp.parseAllGridOptions(mContext)) {
String name = option.name;
if (name != null && name.equals(gridName)) {
match = option;
@@ -255,23 +255,22 @@
return 0;
}
- idp.setCurrentGrid(context, gridName);
+ mIdp.setCurrentGrid(mContext, gridName);
if (Flags.newCustomizationPickerUi()) {
try {
// Wait for device profile to be fully reloaded and applied to the launcher
- loadModelSync(context);
+ loadModelSync(mContext);
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, "Fail to load model", e);
}
}
- context.getContentResolver().notifyChange(uri, null);
+ mContext.getContentResolver().notifyChange(uri, null);
return 1;
}
case ICON_THEMED:
case SET_ICON_THEMED: {
- ThemeManager.INSTANCE.get(context)
- .setMonoThemeEnabled(values.getAsBoolean(BOOLEAN_VALUE));
- context.getContentResolver().notifyChange(uri, null);
+ mThemeManager.setMonoThemeEnabled(values.getAsBoolean(BOOLEAN_VALUE));
+ mContext.getContentResolver().notifyChange(uri, null);
return 1;
}
default:
@@ -298,12 +297,7 @@
@Override
public Bundle call(@NonNull String method, String arg, Bundle extras) {
- Context context = getContext();
- if (context == null) {
- return null;
- }
-
- if (context.checkPermission("android.permission.BIND_WALLPAPER",
+ if (mContext.checkPermission("android.permission.BIND_WALLPAPER",
Binder.getCallingPid(), Binder.getCallingUid())
!= PackageManager.PERMISSION_GRANTED) {
return null;
@@ -317,14 +311,10 @@
}
private synchronized Bundle getPreview(Bundle request) {
- Context context = getContext();
- if (context == null) {
- return null;
- }
RunnableList lifeCycleTracker = new RunnableList();
try {
PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(
- getContext(), lifeCycleTracker, request);
+ mContext, lifeCycleTracker, request);
PreviewLifecycleObserver observer =
new PreviewLifecycleObserver(lifeCycleTracker, renderer);
diff --git a/src/com/android/launcher3/graphics/LauncherCustomizationProvider.kt b/src/com/android/launcher3/graphics/LauncherCustomizationProvider.kt
new file mode 100644
index 0000000..c949e2e
--- /dev/null
+++ b/src/com/android/launcher3/graphics/LauncherCustomizationProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.launcher3.graphics
+
+import android.content.Context
+import android.net.Uri
+import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
+import com.android.launcher3.util.ContentProviderProxy
+
+/** Provider for various Launcher customizations exposed via a ContentProvider API */
+class LauncherCustomizationProvider : ContentProviderProxy() {
+
+ override fun getProxy(ctx: Context): ProxyProvider? = ctx.appComponent.gridCustomizationsProxy
+
+ override fun getType(uri: Uri) = "vnd.android.cursor.dir/launcher_grid"
+}
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index f0d670e..6fe5804 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -124,7 +124,7 @@
if (Flags.newCustomizationPickerUi()) {
updateColorOverrides(bundle);
}
- mHideQsb = bundle.getBoolean(GridCustomizationsProvider.KEY_HIDE_BOTTOM_ROW);
+ mHideQsb = bundle.getBoolean(GridCustomizationsProxy.KEY_HIDE_BOTTOM_ROW);
mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
mWidth = bundle.getInt(KEY_VIEW_WIDTH);
diff --git a/src/com/android/launcher3/graphics/ShapeDelegate.kt b/src/com/android/launcher3/graphics/ShapeDelegate.kt
index df0c8f9..9033eac 100644
--- a/src/com/android/launcher3/graphics/ShapeDelegate.kt
+++ b/src/com/android/launcher3/graphics/ShapeDelegate.kt
@@ -43,7 +43,6 @@
import androidx.graphics.shapes.transformed
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider
import com.android.launcher3.icons.GraphicsUtils
-import com.android.launcher3.icons.IconNormalizer.normalizeAdaptiveIcon
import com.android.launcher3.views.ClipPathView
/** Abstract representation of the shape of an icon shape */
@@ -342,12 +341,5 @@
centerY = (bottom - top) / 2,
rounding = CornerRounding(cornerR),
)
-
- @JvmStatic
- val normalizationScale =
- normalizeAdaptiveIcon(
- AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)),
- AREA_CALC_SIZE,
- )
}
}
diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
index 4a0ff8c..de85460 100644
--- a/src/com/android/launcher3/graphics/ThemeManager.kt
+++ b/src/com/android/launcher3/graphics/ThemeManager.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.content.res.Resources
import com.android.launcher3.EncryptionType
+import com.android.launcher3.Item
import com.android.launcher3.LauncherPrefChangeListener
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.LauncherPrefs.Companion.backedUpItem
@@ -38,11 +39,12 @@
/** Centralized class for managing Launcher icon theming */
@LauncherAppSingleton
-open class ThemeManager
+class ThemeManager
@Inject
constructor(
- @ApplicationContext protected val context: Context,
- protected val prefs: LauncherPrefs,
+ @ApplicationContext private val context: Context,
+ private val prefs: LauncherPrefs,
+ private val iconControllerFactory: IconControllerFactory,
lifecycle: DaggerSingletonTracker,
) {
@@ -72,21 +74,21 @@
val receiver = SimpleBroadcastReceiver(context, MAIN_EXECUTOR) { verifyIconState() }
receiver.registerPkgActions("android", ACTION_OVERLAY_CHANGED)
- val prefListener = LauncherPrefChangeListener { key ->
- when (key) {
- KEY_THEMED_ICONS,
- KEY_ICON_SHAPE -> verifyIconState()
- }
- }
- prefs.addListener(prefListener, THEMED_ICONS, PREF_ICON_SHAPE)
+ val keys = (iconControllerFactory.prefKeys + PREF_ICON_SHAPE)
+ val keysArray = keys.toTypedArray()
+ val prefKeySet = keys.map { it.sharedPrefKey }
+ val prefListener = LauncherPrefChangeListener { key ->
+ if (prefKeySet.contains(key)) verifyIconState()
+ }
+ prefs.addListener(prefListener, *keysArray)
lifecycle.addCloseable {
receiver.unregisterReceiverSafely()
- prefs.removeListener(prefListener)
+ prefs.removeListener(prefListener, *keysArray)
}
}
- protected fun verifyIconState() {
+ private fun verifyIconState() {
val newState = parseIconState(iconState)
if (newState == iconState) return
iconState = newState
@@ -126,17 +128,13 @@
return IconState(
iconMask = iconMask,
folderShapeMask = folderShapeMask,
- themeController = createThemeController(),
+ themeController = iconControllerFactory.createThemeController(),
iconScale = shapeModel?.iconScale ?: 1f,
iconShape = iconShape,
folderShape = folderShape,
)
}
- protected open fun createThemeController(): IconThemeController? {
- return if (isMonoThemeEnabled) MONO_THEME_CONTROLLER else null
- }
-
data class IconState(
val iconMask: String,
val folderShapeMask: String,
@@ -154,6 +152,15 @@
fun onThemeChanged()
}
+ open class IconControllerFactory @Inject constructor(protected val prefs: LauncherPrefs) {
+
+ open val prefKeys: List<Item> = listOf(THEMED_ICONS)
+
+ open fun createThemeController(): IconThemeController? {
+ return if (prefs.get(THEMED_ICONS)) MONO_THEME_CONTROLLER else null
+ }
+ }
+
companion object {
@JvmField val INSTANCE = DaggerSingletonObject(LauncherAppComponent::getThemeManager)
diff --git a/src/com/android/launcher3/icons/CacheableShortcutInfo.kt b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt
index 225e12f..50dd146 100644
--- a/src/com/android/launcher3/icons/CacheableShortcutInfo.kt
+++ b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt
@@ -30,6 +30,7 @@
import com.android.launcher3.icons.cache.BaseIconCache
import com.android.launcher3.icons.cache.CachingLogic
import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ApiWrapper
import com.android.launcher3.util.ApplicationInfoWrapper
import com.android.launcher3.util.PackageUserKey
import com.android.launcher3.util.Themes
@@ -114,7 +115,15 @@
d,
IconOptions()
.setExtractedColor(Themes.getColorAccent(context))
- .setSourceHint(getSourceHint(info, cache)),
+ .setSourceHint(
+ getSourceHint(info, cache)
+ .copy(
+ isFileDrawable =
+ ApiWrapper.INSTANCE[context].isFileDrawable(
+ info.shortcutInfo
+ )
+ )
+ ),
)
} ?: BitmapInfo.LOW_RES_INFO
}
diff --git a/src/com/android/launcher3/icons/LauncherIcons.kt b/src/com/android/launcher3/icons/LauncherIcons.kt
index 6c018e8..29c0de1 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.kt
+++ b/src/com/android/launcher3/icons/LauncherIcons.kt
@@ -66,11 +66,16 @@
return userCache.getUserInfo(user)
}
- public override fun getShapePath(drawable: AdaptiveIconDrawable, iconBounds: Rect): Path {
- if (!Flags.enableLauncherIconShapes()) return drawable.iconMask
+ override fun getShapePath(drawable: AdaptiveIconDrawable, iconBounds: Rect): Path {
+ if (!Flags.enableLauncherIconShapes()) return super.getShapePath(drawable, iconBounds)
return themeManager.iconShape.getPath(iconBounds)
}
+ override fun getIconScale(): Float {
+ if (!Flags.enableLauncherIconShapes()) return super.getIconScale()
+ return themeManager.iconState.iconScale
+ }
+
override fun drawAdaptiveIcon(
canvas: Canvas,
drawable: AdaptiveIconDrawable,
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 9a1c874..2f1af68 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -905,6 +905,10 @@
@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),
+
+ @UiEvent(doc = "Time passed between nav handle touch down and cancellation without "
+ + "triggering Contextual Search")
+ LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON(2171),
;
private final int mId;
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 003bef3..3ee8b87 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -167,7 +167,7 @@
return;
}
Map<PackageItemInfo, List<WidgetItem>>
- widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItem();
+ widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItemForPicker();
List<WidgetsListBaseEntry> widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext())
.build(widgetsByPackageItem);
Predicate<WidgetItem> filter = mBgDataModel.widgetsModel.getDefaultWidgetsFilter();
diff --git a/src/com/android/launcher3/model/ModelInitializer.kt b/src/com/android/launcher3/model/ModelInitializer.kt
new file mode 100644
index 0000000..69a320a
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelInitializer.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2025 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.model
+
+import android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED
+import android.content.ComponentName
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.pm.LauncherApps
+import android.content.pm.LauncherApps.ArchiveCompatibilityParams
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.Flags
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener
+import com.android.launcher3.LauncherModel
+import com.android.launcher3.LauncherPrefs.Companion.getPrefs
+import com.android.launcher3.Utilities
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.graphics.ThemeManager
+import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.LauncherIconProvider
+import com.android.launcher3.icons.LauncherIcons.IconPool
+import com.android.launcher3.notification.NotificationListener
+import com.android.launcher3.pm.InstallSessionHelper
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.android.launcher3.util.SafeCloseable
+import com.android.launcher3.util.SettingsCache
+import com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI
+import com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI
+import com.android.launcher3.util.SimpleBroadcastReceiver
+import com.android.launcher3.widget.custom.CustomWidgetManager
+import java.util.function.Consumer
+
+/** Utility class for initializing all model callbacks */
+class ModelInitializer(
+ @ApplicationContext private val context: Context,
+ private val iconPool: IconPool,
+ private val iconCache: IconCache,
+ private val idp: InvariantDeviceProfile,
+ private val themeManager: ThemeManager,
+ private val userCache: UserCache,
+ private val settingsCache: SettingsCache,
+ private val iconProvider: LauncherIconProvider,
+ private val customWidgetManager: CustomWidgetManager,
+ private val installSessionHelper: InstallSessionHelper,
+ private val closeActions: Consumer<SafeCloseable>,
+) {
+
+ fun initialize(model: LauncherModel) {
+ fun refreshAndReloadLauncher() {
+ iconPool.clear()
+ iconCache.updateIconParams(idp.fillResIconDpi, idp.iconBitmapSize)
+ model.forceReload()
+ }
+
+ // IDP changes
+ val idpChangeListener = OnIDPChangeListener { modelChanged ->
+ if (modelChanged) refreshAndReloadLauncher()
+ }
+ idp.addOnChangeListener(idpChangeListener)
+ closeActions.accept { idp.removeOnChangeListener(idpChangeListener) }
+
+ // Theme changes
+ val themeChangeListener = ThemeChangeListener { refreshAndReloadLauncher() }
+ themeManager.addChangeListener(themeChangeListener)
+ closeActions.accept { themeManager.removeChangeListener(themeChangeListener) }
+
+ // System changes
+ val modelCallbacks = model.newModelCallbacks()
+ val launcherApps = context.getSystemService(LauncherApps::class.java)!!
+ launcherApps.registerCallback(modelCallbacks)
+ closeActions.accept { launcherApps.unregisterCallback(modelCallbacks) }
+
+ if (Utilities.ATLEAST_V && Flags.enableSupportForArchiving()) {
+ launcherApps.setArchiveCompatibility(
+ ArchiveCompatibilityParams().apply {
+ setEnableUnarchivalConfirmation(false)
+ setEnableIconOverlay(!Flags.useNewIconForArchivedApps())
+ }
+ )
+ }
+
+ // Device profile policy changes
+ val dpUpdateReceiver =
+ SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR) { model.reloadStringCache() }
+ dpUpdateReceiver.register(ACTION_DEVICE_POLICY_RESOURCE_UPDATED)
+ closeActions.accept { dpUpdateReceiver.unregisterReceiverSafely() }
+
+ // Development helper
+ if (BuildConfig.IS_STUDIO_BUILD) {
+ val reloadReceiver =
+ SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR) { model.forceReload() }
+ reloadReceiver.register(Context.RECEIVER_EXPORTED, ACTION_FORCE_RELOAD)
+ closeActions.accept { reloadReceiver.unregisterReceiverSafely() }
+ }
+
+ // User changes
+ closeActions.accept(userCache.addUserEventListener(model::onUserEvent))
+
+ // Private space settings changes
+ val psSettingsListener = SettingsCache.OnChangeListener { model.forceReload() }
+ settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psSettingsListener)
+ closeActions.accept {
+ settingsCache.unregister(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psSettingsListener)
+ }
+
+ // Notification dots changes
+ val notificationChanges =
+ SettingsCache.OnChangeListener { dotsEnabled ->
+ if (dotsEnabled)
+ NotificationListener.requestRebind(
+ ComponentName(context, NotificationListener::class.java)
+ )
+ }
+ settingsCache.register(NOTIFICATION_BADGING_URI, notificationChanges)
+ notificationChanges.onSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI))
+ closeActions.accept {
+ settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationChanges)
+ }
+
+ // removable smartspace
+ if (Flags.enableSmartspaceRemovalToggle()) {
+ val smartSpacePrefChanges =
+ SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ if (LoaderTask.SMARTSPACE_ON_HOME_SCREEN == key) model.forceReload()
+ }
+ getPrefs(context).registerOnSharedPreferenceChangeListener(smartSpacePrefChanges)
+ closeActions.accept {
+ getPrefs(context).unregisterOnSharedPreferenceChangeListener(smartSpacePrefChanges)
+ }
+ }
+
+ // Custom widgets
+ closeActions.accept(customWidgetManager.addWidgetRefreshCallback(model::rebindCallbacks))
+
+ // Icon changes
+ closeActions.accept(
+ iconProvider.registerIconChangeListener(model::onAppIconChanged, MODEL_EXECUTOR.handler)
+ )
+
+ // Install session changes
+ closeActions.accept(installSessionHelper.registerInstallTracker(modelCallbacks))
+ }
+
+ companion object {
+ private const val ACTION_FORCE_RELOAD = "force-reload-launcher"
+ }
+}
diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt
index 40ea17d..6e3e35e 100644
--- a/src/com/android/launcher3/model/ModelTaskController.kt
+++ b/src/com/android/launcher3/model/ModelTaskController.kt
@@ -77,7 +77,7 @@
}
fun bindUpdatedWidgets(dataModel: BgDataModel) {
- val widgetsByPackageItem = dataModel.widgetsModel.widgetsByPackageItem
+ val widgetsByPackageItem = dataModel.widgetsModel.widgetsByPackageItemForPicker
val allWidgets = WidgetsListBaseEntriesBuilder(app.context).build(widgetsByPackageItem)
val defaultWidgetsFilter = dataModel.widgetsModel.defaultWidgetsFilter
diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java
index a176465..ab960d8 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -70,6 +70,7 @@
private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsByPackageItem = new HashMap<>();
@Nullable private Predicate<WidgetItem> mDefaultWidgetsFilter = null;
@Nullable private Predicate<WidgetItem> mPredictedWidgetsFilter = null;
+ @Nullable private WidgetValidityCheckForPicker mWidgetValidityCheckForPicker = null;
/**
* Returns all widgets keyed by their component key.
@@ -87,13 +88,44 @@
}
/**
- * Returns widgets grouped by the package item that they should belong to.
+ * Returns widgets (eligible for display in picker) keyed by their component key.
*/
- public synchronized Map<PackageItemInfo, List<WidgetItem>> getWidgetsByPackageItem() {
- if (!WIDGETS_ENABLED) {
+ public synchronized Map<ComponentKey, WidgetItem> getWidgetsByComponentKeyForPicker() {
+ if (!WIDGETS_ENABLED || mWidgetValidityCheckForPicker == null) {
return Collections.emptyMap();
}
- return new HashMap<>(mWidgetsByPackageItem);
+
+ return mWidgetsByPackageItem.values().stream()
+ .flatMap(Collection::stream).distinct()
+ .filter(widgetItem -> mWidgetValidityCheckForPicker.test(widgetItem))
+ .collect(Collectors.toMap(
+ widget -> new ComponentKey(widget.componentName, widget.user),
+ Function.identity()
+ ));
+ }
+
+ /**
+ * Returns widgets (displayable in the widget picker) grouped by the package item that
+ * they should belong to.
+ */
+ public synchronized Map<PackageItemInfo, List<WidgetItem>> getWidgetsByPackageItemForPicker() {
+ if (!WIDGETS_ENABLED || mWidgetValidityCheckForPicker == null) {
+ return Collections.emptyMap();
+ }
+
+ return mWidgetsByPackageItem.entrySet().stream()
+ .collect(
+ Collectors.toMap(
+ Map.Entry::getKey,
+ entry -> entry.getValue().stream()
+ .filter(widgetItem ->
+ mWidgetValidityCheckForPicker.test(widgetItem))
+ .collect(Collectors.toList())
+ )
+ )
+ .entrySet().stream()
+ .filter(entry -> !entry.getValue().isEmpty())
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/**
@@ -181,6 +213,9 @@
Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size());
}
+ // Refresh the validity checker with latest app state.
+ mWidgetValidityCheckForPicker = new WidgetValidityCheckForPicker(app);
+
// Temporary cache for {@link PackageItemInfos} to avoid having to go through
// {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList}
PackageItemInfoCache packageItemInfoCache = new PackageItemInfoCache();
@@ -195,7 +230,6 @@
// add and update.
mWidgetsByPackageItem.putAll(rawWidgetsShortcuts.stream()
- .filter(new WidgetValidityCheck(app))
.filter(new WidgetFlagCheck())
.flatMap(widgetItem -> getPackageUserKeys(app.getContext(), widgetItem).stream()
.map(key -> new Pair<>(packageItemInfoCache.getOrCreate(key), widgetItem)))
@@ -270,12 +304,15 @@
return packageUserKeys;
}
- private static class WidgetValidityCheck implements Predicate<WidgetItem> {
+ /**
+ * Checks if widgets are eligible for displaying in widget picker / tray.
+ */
+ private static class WidgetValidityCheckForPicker implements Predicate<WidgetItem> {
private final InvariantDeviceProfile mIdp;
private final AppFilter mAppFilter;
- WidgetValidityCheck(LauncherAppState app) {
+ WidgetValidityCheckForPicker(LauncherAppState app) {
mIdp = app.getInvariantDeviceProfile();
mAppFilter = new AppFilter(app.getContext());
}
@@ -310,6 +347,10 @@
}
}
+ /**
+ * Checks if certain widgets that are available behind flag can be used across all surfaces in
+ * launcher.
+ */
private static class WidgetFlagCheck implements Predicate<WidgetItem> {
private static final String BUBBLES_SHORTCUT_WIDGET =
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index b9c928c..7451ce2 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -34,13 +34,15 @@
import com.android.launcher3.Flags;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.SafeCloseable;
import java.lang.ref.WeakReference;
import java.util.Objects;
@SuppressWarnings("NewApi")
@WorkerThread
-public class InstallSessionTracker extends PackageInstaller.SessionCallback {
+public class InstallSessionTracker extends PackageInstaller.SessionCallback implements
+ SafeCloseable {
public static final String TAG = "InstallSessionTracker";
@@ -196,7 +198,8 @@
}
}
- public void unregister() {
+ @Override
+ public void close() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
mInstaller.unregisterSessionCallback(this);
} else {
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index b748011..bc5064d 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -110,13 +110,16 @@
public static <T extends Context & ActivityContext> Runnable createUpdateRunnable(
final T context,
final ItemInfo originalInfo,
- final Handler uiHandler, final PopupContainerWithArrow container,
- final List<DeepShortcutView> shortcutViews) {
+ final Handler uiHandler,
+ final PopupContainerWithArrow container,
+ final List<DeepShortcutView> shortcutViews
+ ) {
final ComponentName activity = originalInfo.getTargetComponent();
final UserHandle user = originalInfo.user;
+ final String targetPackage = originalInfo.getTargetPackage();
return () -> {
ApplicationInfoWrapper infoWrapper =
- new ApplicationInfoWrapper(context, originalInfo.getTargetPackage(), user);
+ new ApplicationInfoWrapper(context, targetPackage, user);
List<ShortcutInfo> shortcuts = new ShortcutRequest(context, user)
.withContainer(activity)
.query(ShortcutRequest.PUBLISHED);
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 34c9117..0da431f 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -51,6 +51,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
+import com.android.launcher3.Flags;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherFiles;
@@ -130,9 +131,11 @@
removeOldDBs(context, oldPhoneFileName);
// The idp before this contains data about the old phone, after this it becomes the idp
// of the current phone.
- FileLog.d(TAG, "Resetting IDP to default for restore dest device");
- idp.reset(context);
- trySettingPreviousGridAsCurrent(context, idp, oldPhoneFileName, previousDbs);
+ if (!Flags.oneGridSpecs()) {
+ FileLog.d(TAG, "Resetting IDP to default for restore dest device");
+ idp.reset(context);
+ trySettingPreviousGridAsCurrent(context, idp, oldPhoneFileName, previousDbs);
+ }
}
diff --git a/src/com/android/launcher3/shapes/ShapesProvider.kt b/src/com/android/launcher3/shapes/ShapesProvider.kt
index 463c816..5427c89 100644
--- a/src/com/android/launcher3/shapes/ShapesProvider.kt
+++ b/src/com/android/launcher3/shapes/ShapesProvider.kt
@@ -193,15 +193,6 @@
"M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
folderPathString = folderShapes["arch"]!!,
),
- "sunny" to
- IconShapeModel(
- key = "sunny",
- title = "sunny",
- pathString =
- "M42.846 4.873C46.084 -.531 53.916 -.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C -.531 53.916 -.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
- folderPathString = folderShapes["clover"]!!,
- iconScale = 72f / 92f,
- ),
)
} else {
mapOf(
diff --git a/src/com/android/launcher3/testing/TestInformationProvider.java b/src/com/android/launcher3/testing/TestInformationProvider.java
index 17b472a..4b592e7 100644
--- a/src/com/android/launcher3/testing/TestInformationProvider.java
+++ b/src/com/android/launcher3/testing/TestInformationProvider.java
@@ -16,61 +16,40 @@
package com.android.launcher3.testing;
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.net.Uri;
+import android.content.Context;
import android.os.Bundle;
import android.util.Log;
-import com.android.launcher3.Utilities;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
-public class TestInformationProvider extends ContentProvider {
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.ContentProviderProxy;
+
+public class TestInformationProvider extends ContentProviderProxy {
private static final String TAG = "TestInformationProvider";
+ @Nullable
@Override
- public boolean onCreate() {
- return true;
- }
-
- @Override
- public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
- return 0;
- }
-
- @Override
- public int delete(Uri uri, String s, String[] strings) {
- return 0;
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues contentValues) {
- return null;
- }
-
- @Override
- public String getType(Uri uri) {
- return null;
- }
-
- @Override
- public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) {
- return null;
- }
-
- @Override
- public Bundle call(String method, String arg, Bundle extras) {
+ public ProxyProvider getProxy(@NonNull Context context) {
if (Utilities.isRunningInTestHarness()) {
- TestInformationHandler handler = TestInformationHandler.newInstance(getContext());
- handler.init(getContext());
+ return new ProxyProvider() {
+ @Nullable
+ @Override
+ public Bundle call(@NonNull String method, @Nullable String arg,
+ @Nullable Bundle extras) {
+ TestInformationHandler handler = TestInformationHandler.newInstance(context);
+ handler.init(context);
- Bundle response = handler.call(method, arg, extras);
- if (response == null) {
- Log.e(TAG, "Couldn't handle method: " + method + "; current handler="
- + handler.getClass().getSimpleName());
- }
- return response;
+ Bundle response = handler.call(method, arg, extras);
+ if (response == null) {
+ Log.e(TAG, "Couldn't handle method: " + method + "; current handler="
+ + handler.getClass().getSimpleName());
+ }
+ return response;
+ }
+ };
}
return null;
}
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
index 107bcc1..2cc4909 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -207,6 +207,11 @@
}
config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
+ if (launcher.getDeviceProfile().isPhone) {
+ config.setInterpolator(ANIM_WORKSPACE_FADE, INSTANT);
+ config.setInterpolator(ANIM_HOTSEAT_FADE, INSTANT);
+ config.animFlags |= StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+ }
} else {
if (config.isUserControlled()) {
config.setInterpolator(ANIM_DEPTH, Interpolators.reverse(BLUR_MANUAL));
@@ -248,6 +253,11 @@
}
config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
+ if (launcher.getDeviceProfile().isPhone) {
+ config.setInterpolator(ANIM_WORKSPACE_FADE, FINAL_FRAME);
+ config.setInterpolator(ANIM_HOTSEAT_FADE, FINAL_FRAME);
+ config.animFlags |= StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+ }
} else {
config.setInterpolator(ANIM_DEPTH,
config.isUserControlled() ? BLUR_MANUAL : BLUR_ATOMIC);
diff --git a/src/com/android/launcher3/util/ApiWrapper.java b/src/com/android/launcher3/util/ApiWrapper.java
index 56337b0..0510d59 100644
--- a/src/com/android/launcher3/util/ApiWrapper.java
+++ b/src/com/android/launcher3/util/ApiWrapper.java
@@ -216,6 +216,13 @@
return 0;
}
+ /**
+ * Checks if the shortcut is using an icon with file or URI source
+ */
+ public boolean isFileDrawable(@NonNull ShortcutInfo shortcutInfo) {
+ return false;
+ }
+
private static class NoopDrawable extends ColorDrawable {
@Override
public int getIntrinsicHeight() {
diff --git a/src/com/android/launcher3/util/ContentProviderProxy.kt b/src/com/android/launcher3/util/ContentProviderProxy.kt
new file mode 100644
index 0000000..db693db
--- /dev/null
+++ b/src/com/android/launcher3/util/ContentProviderProxy.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2025 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.util
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.os.Bundle
+
+/** Wrapper around [ContentProvider] which allows delegating all calls to an interface */
+abstract class ContentProviderProxy : ContentProvider() {
+
+ override fun onCreate() = true
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =
+ checkGetProxy()?.delete(uri, selection, selectionArgs) ?: 0
+
+ /** Do not route this call through proxy as it doesn't generally require initializing objects */
+ override fun getType(uri: Uri): String? = null
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? =
+ checkGetProxy()?.insert(uri, values)
+
+ override fun query(
+ uri: Uri,
+ projection: Array<out String>?,
+ selection: String?,
+ selectionArgs: Array<out String>?,
+ sortOrder: String?,
+ ): Cursor? = checkGetProxy()?.query(uri, projection, selection, selectionArgs, sortOrder)
+
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array<out String>?,
+ ): Int = checkGetProxy()?.update(uri, values, selection, selectionArgs) ?: 0
+
+ override fun call(method: String, arg: String?, extras: Bundle?): Bundle? =
+ checkGetProxy()?.call(method, arg, extras)
+
+ private fun checkGetProxy(): ProxyProvider? = context?.let { getProxy(it) }
+
+ abstract fun getProxy(ctx: Context): ProxyProvider?
+
+ /** Interface for handling the actual content provider calls */
+ interface ProxyProvider {
+
+ fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
+
+ fun insert(uri: Uri, values: ContentValues?): Uri? = null
+
+ fun query(
+ uri: Uri,
+ projection: Array<out String>?,
+ selection: String?,
+ selectionArgs: Array<out String>?,
+ sortOrder: String?,
+ ): Cursor? = null
+
+ fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array<out String>?,
+ ): Int = 0
+
+ fun call(method: String, arg: String?, extras: Bundle?): Bundle? = null
+ }
+}
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 647d170..11f0bc2 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -509,7 +509,17 @@
* @param isInDesktopModeAndNotInOverview True if a desktop is currently active on the given
* display, and Overview is currently inactive.
*/
- void onIsInDesktopModeChanged(int displayId, boolean isInDesktopModeAndNotInOverview);
+ default void onIsInDesktopModeChanged(int displayId,
+ boolean isInDesktopModeAndNotInOverview) {
+ }
+
+ /**
+ * Called whenever the conditions that allow the creation of desks change.
+ *
+ * @param canCreateDesks whether it is possible to create new desks.
+ */
+ default void onCanCreateDesksChanged(boolean canCreateDesks) {
+ }
}
}
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index ddf18df..a295d6b 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -20,6 +20,7 @@
import static com.android.launcher3.Utilities.boundToRange;
import static com.android.launcher3.Utilities.mapToRange;
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
import static java.lang.Math.max;
@@ -44,10 +45,10 @@
import androidx.core.util.Consumer;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
-import com.android.launcher3.graphics.ShapeDelegate;
import com.android.launcher3.graphics.ThemeManager;
/**
@@ -68,6 +69,7 @@
private boolean mIsAdaptiveIcon = false;
private ValueAnimator mRevealAnimator;
+ private float mIconScale;
private final Rect mStartRevealRect = new Rect();
private final Rect mEndRevealRect = new Rect();
@@ -173,9 +175,12 @@
mTaskCornerRadius = cornerRadius / scale;
if (mIsAdaptiveIcon) {
- if (!isOpening && progress >= shapeProgressStart) {
+ final ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
+ mIconScale = themeManager.getIconState().getIconScale();
+ if ((!isOpening || Flags.enableLauncherIconShapes())
+ && progress >= shapeProgressStart) {
if (mRevealAnimator == null) {
- mRevealAnimator = ThemeManager.INSTANCE.get(getContext()).getIconShape()
+ mRevealAnimator = themeManager.getIconShape()
.createRevealAnimator(this, mStartRevealRect,
mOutline, mTaskCornerRadius, !isOpening);
mRevealAnimator.addListener(forEndCallback(() -> mRevealAnimator = null));
@@ -259,8 +264,7 @@
mStartRevealRect.set(0, 0, originalWidth, originalHeight);
if (!isFolderIcon) {
- Utilities.scaleRectAboutCenter(mStartRevealRect,
- ShapeDelegate.getNormalizationScale());
+ Utilities.scaleRectAboutCenter(mStartRevealRect, ICON_VISIBLE_AREA_FACTOR);
}
if (dp.isLandscape) {
@@ -310,17 +314,24 @@
@Override
public void draw(Canvas canvas) {
- int count = canvas.save();
+ int count1 = canvas.save();
if (mClipPath != null) {
canvas.clipPath(mClipPath);
}
- super.draw(canvas);
+ int count2 = canvas.save();
+ float iconCenterX =
+ (mFinalDrawableBounds.right - mFinalDrawableBounds.left) / 2f * mIconScale;
+ float iconCenterY =
+ (mFinalDrawableBounds.bottom - mFinalDrawableBounds.top) / 2f * mIconScale;
+ canvas.scale(mIconScale, mIconScale, iconCenterX, iconCenterY);
if (mBackground != null) {
mBackground.draw(canvas);
}
if (mForeground != null) {
mForeground.draw(canvas);
}
+ canvas.restoreToCount(count2);
+ super.draw(canvas);
if (mTaskViewArtist != null) {
canvas.saveLayerAlpha(
0,
@@ -334,7 +345,7 @@
canvas.scale(drawScale, drawScale);
mTaskViewArtist.taskViewDrawCallback.accept(canvas);
}
- canvas.restoreToCount(count);
+ canvas.restoreToCount(count1);
}
void recycle() {
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 22857b1..5b3abc3 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -55,7 +55,7 @@
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.graphics.PreloadIconDrawable;
import com.android.launcher3.icons.FastBitmapDrawable;
-import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.popup.SystemShortcut;
@@ -463,10 +463,7 @@
Rect bounds = new Rect(0, 0, (int) position.width() + blurSizeOutline,
(int) position.height() + blurSizeOutline);
bounds.inset(blurSizeOutline / 2, blurSizeOutline / 2);
-
- try (LauncherIcons li = LauncherIcons.obtain(l)) {
- Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(drawable));
- }
+ Utilities.scaleRectAboutCenter(bounds, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
bounds.inset(
(int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
diff --git a/src/com/android/launcher3/widget/picker/OWNERS b/src/com/android/launcher3/widget/picker/OWNERS
index 6aabbfa..991193f 100644
--- a/src/com/android/launcher3/widget/picker/OWNERS
+++ b/src/com/android/launcher3/widget/picker/OWNERS
@@ -6,7 +6,6 @@
#
# Widget Picker OWNERS
-zakcohen@google.com
shamalip@google.com
wvk@google.com
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 8cdf380..3658989 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -364,7 +364,13 @@
context.assets.open("dumpTests/$fileName").bufferedReader().use(BufferedReader::readText)
private fun writeToDevice(context: Context, fileName: String, content: String) {
- File(context.getDir("dumpTests", Context.MODE_PRIVATE), fileName).writeText(content)
+ val dir =
+ File(context.filesDir, "dumpTests").also {
+ if (!it.exists()) {
+ it.mkdirs()
+ }
+ }
+ File(dir, fileName).writeText(content)
}
protected fun Float.dpToPx(): Float {
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
index ae4ff04..d704195 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
@@ -119,6 +119,11 @@
// A widget in different package (none of that app's widgets are in widget
// sections xml)
createAppWidgetProviderInfo(AppBTestWidgetComponent),
+ // A widget in different app that is meant to be hidden from picker
+ createAppWidgetProviderInfo(
+ AppCPinOnlyTestWidgetComponent,
+ /*hideFromPicker=*/ true,
+ ),
)
)
@@ -129,12 +134,13 @@
}
@Test
- fun widgetsByPackage_treatsWidgetSectionsAsSeparatePackageItems() {
+ fun widgetsByPackageForPicker_treatsWidgetSectionsAsSeparatePackageItems() {
loadWidgets()
- val packages: Map<PackageItemInfo, List<WidgetItem>> = underTest.widgetsByPackageItem
+ val packages: Map<PackageItemInfo, List<WidgetItem>> =
+ underTest.widgetsByPackageItemForPicker
- // expect 3 package items
+ // expect 3 package items (no app C as its widget is hidden from picker)
// one for the custom section with widget from appA
// one for package section for second widget from appA (that wasn't listed in xml)
// and one for package section for appB
@@ -167,6 +173,13 @@
assertThat(appBPackageSection).hasSize(1)
val widgetsInAppBSection = appBPackageSection.entries.first().value
assertThat(widgetsInAppBSection).hasSize(1)
+
+ // No App C's package section - as the only widget hosted by it is hidden in picker
+ val appCPackageSection =
+ packageSections.filter {
+ it.key.packageName == AppCPinOnlyTestWidgetComponent.packageName
+ }
+ assertThat(appCPackageSection).isEmpty()
}
@Test
@@ -175,7 +188,29 @@
val widgetsByComponentKey: Map<ComponentKey, WidgetItem> = underTest.widgetsByComponentKey
+ // Has all widgets including ones not visible in picker
+ assertThat(widgetsByComponentKey).hasSize(4)
+ widgetsByComponentKey.forEach { entry ->
+ assertThat(entry.key).isEqualTo(entry.value as ComponentKey)
+ }
+ }
+
+ @Test
+ fun widgetComponentMapForPicker_excludesWidgetsHiddenInPicker() {
+ loadWidgets()
+
+ val widgetsByComponentKey: Map<ComponentKey, WidgetItem> =
+ underTest.widgetsByComponentKeyForPicker
+
+ // Has all widgets excluding the appC's widget.
assertThat(widgetsByComponentKey).hasSize(3)
+ assertThat(
+ widgetsByComponentKey.filter {
+ it.key.componentName == AppCPinOnlyTestWidgetComponent
+ }
+ )
+ .isEmpty()
+ // widgets mapped correctly
widgetsByComponentKey.forEach { entry ->
assertThat(entry.key).isEqualTo(entry.value as ComponentKey)
}
@@ -189,7 +224,7 @@
}
@Test
- fun getWidgetsByPackageItem_returnsACopyOfMap() {
+ fun getWidgetsByPackageItemForPicker_returnsACopyOfMap() {
loadWidgets()
val latch = CountDownLatch(1)
@@ -198,8 +233,8 @@
// each "widgetsByPackageItem" read returns a different copy of the map held internally.
// Modifying one shouldn't impact another.
- for ((_, _) in underTest.widgetsByPackageItem.entries) {
- underTest.widgetsByPackageItem.clear()
+ for ((_, _) in underTest.widgetsByPackageItemForPicker.entries) {
+ underTest.widgetsByPackageItemForPicker.clear()
if (update) { // trigger update
update = false
// Similarly, model could update its code independently while a client is
@@ -256,6 +291,9 @@
private val AppBTestWidgetComponent: ComponentName =
ComponentName.createRelative("com.test.package", "TestProvider")
+ private val AppCPinOnlyTestWidgetComponent: ComponentName =
+ ComponentName.createRelative("com.testC.package", "PinOnlyTestProvider")
+
private const val LOAD_WIDGETS_TIMEOUT_SECONDS = 2L
}
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
index 15a9964..23c1da9 100644
--- a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
@@ -219,7 +219,7 @@
.whenever(launcherApps)
.unregisterPackageInstallerSessionCallback(installSessionTracker)
// When
- installSessionTracker.unregister()
+ installSessionTracker.close()
// Then
verify(launcherApps).unregisterPackageInstallerSessionCallback(installSessionTracker)
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
index 508c9a4..49d305b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
@@ -63,15 +63,6 @@
@Test
@EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
- fun `verify valid path sunny`() {
- ShapesProvider.iconShapes["sunny"]?.apply {
- GenericPathShape(pathString)
- PathParser.createPathFromPathData(pathString)
- }
- }
-
- @Test
- @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
fun `verify valid path circle`() {
ShapesProvider.iconShapes["circle"]?.apply {
GenericPathShape(pathString)
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java b/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java
index a87a208..9fbd7ff 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java
@@ -15,6 +15,8 @@
*/
package com.android.launcher3.util;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_HIDE_FROM_PICKER;
+
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
@@ -83,14 +85,30 @@
/**
* Creates a {@link AppWidgetProviderInfo} for the provided component name
+ *
+ * @param cn component name of the appwidget provider
+ * @param hideFromPicker indicates if the widget should appear in widget picker
*/
- public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn) {
+ public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn,
+ boolean hideFromPicker) {
ActivityInfo activityInfo = new ActivityInfo();
activityInfo.applicationInfo = new ApplicationInfo();
activityInfo.applicationInfo.uid = Process.myUid();
AppWidgetProviderInfo info = new AppWidgetProviderInfo();
+ if (hideFromPicker) {
+ info.widgetFeatures = WIDGET_FEATURE_HIDE_FROM_PICKER;
+ }
info.providerInfo = activityInfo;
info.provider = cn;
return info;
}
+
+ /**
+ * Creates a {@link AppWidgetProviderInfo} for the provided component name
+ *
+ * @param cn component name of the appwidget provider
+ */
+ public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn) {
+ return createAppWidgetProviderInfo(cn, /*hideFromPicker=*/ false);
+ }
}
diff --git a/tests/src/com/android/launcher3/widget/picker/OWNERS b/tests/src/com/android/launcher3/widget/picker/OWNERS
index 775b0c7..716ab90 100644
--- a/tests/src/com/android/launcher3/widget/picker/OWNERS
+++ b/tests/src/com/android/launcher3/widget/picker/OWNERS
@@ -5,7 +5,6 @@
#
# Widget Picker OWNERS
-zakcohen@google.com
shamalip@google.com
wvk@google.com
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index ef72a0f..f633d48 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -157,12 +157,7 @@
LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"dismissing all tasks")) {
final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all");
- for (int i = 0;
- i < FLINGS_FOR_DISMISS_LIMIT
- && !verifyActiveContainer().hasObject(clearAllSelector);
- ++i) {
- flingForwardImpl();
- }
+ flingForwardUntilClearAllVisible();
final Runnable clickClearAll = () -> mLauncher.clickLauncherObject(
mLauncher.waitForObjectInContainer(verifyActiveContainer(),
@@ -183,6 +178,17 @@
}
/**
+ * Scrolls until Clear-all button is visible.
+ */
+ public void flingForwardUntilClearAllVisible() {
+ final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all");
+ for (int i = 0; i < FLINGS_FOR_DISMISS_LIMIT
+ && !verifyActiveContainer().hasObject(clearAllSelector); ++i) {
+ flingForwardImpl();
+ }
+ }
+
+ /**
* Touch to the right of current task. This should dismiss overview and go back to Workspace.
*/
public Workspace touchOutsideFirstTask() {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 4e94e1e..8fbb5e3 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -125,7 +125,7 @@
return right - left;
}
- int getTaskCenterX() {
+ public int getTaskCenterX() {
return mTask.getVisibleCenter().x;
}