Merge "Move flags to desktopmodeflag" into main
diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml
index 885bdb9..2dea79c 100644
--- a/quickstep/res/layout/keyboard_quick_switch_view.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_view.xml
@@ -18,6 +18,8 @@
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/keyboard_quick_switch_view"
+ android:contentDescription="@string/quick_switch_content_description"
+ android:accessibilityPaneTitle="@string/quick_switch_pane_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyboard_quick_switch_margin_top"
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 65f4b3c..7578bd5 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -325,6 +325,12 @@
<!-- Label for creating an application bubble (from the Taskbar only). -->
<string name="open_app_as_a_bubble">Open app as a bubble</string>
+ <!-- Accessibility pane title for quick switch view, which lists apps opened by the user, ordered by how recently the app was opened. -->
+ <string name="quick_switch_pane_title">Recent apps</string>
+
+ <!-- Content description for the quick switch view, which lists apps opened by the user, ordered by how recently the app was opened. -->
+ <string name="quick_switch_content_description">Recent app list</string>
+
<!-- Label for quick switch tile showing how many more apps are available. The number will be displayed above this text. [CHAR LIMIT=NONE] -->
<string name="quick_switch_overflow">{count, plural,
=1{more app}
@@ -336,6 +342,8 @@
<!-- Accessibility label for quick switch tiles showing split tasks [CHAR LIMIT=NONE] -->
<string name="quick_switch_split_task"><xliff:g id="app_name_1" example="Chrome">%1$s</xliff:g> and <xliff:g id="app_name_2" example="Gmail">%2$s</xliff:g></string>
+ <!-- Accessibility label for quick switch tiles that include information about the tile's position in the parent list [CHAR LIMIT=NONE] -->
+ <string name="quick_switch_task_with_position_in_parent"><xliff:g id="task_description" example="Chrome">%1$s</xliff:g>, item <xliff:g id="index_in_parent" example="1">%2$d</xliff:g> of <xliff:g id="total_tasks" example="5">%3$d</xliff:g></string>
<!-- Accessibility label for an arrow button within quick switch UI that scrolls the quick switch content left
TODO(b/397975686): Make these translatable when verified by UX. -->
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 7cf0605..aae8a56 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -352,7 +352,7 @@
new RemoteAnimationAdapter(runner, duration, statusBarTransitionDelay),
new RemoteTransition(runner.toRemoteTransition(),
mLauncher.getIApplicationThread(), "QuickstepLaunch"));
- IRemoteCallback endCallback = completeRunnableListCallback(onEndCallback);
+ IRemoteCallback endCallback = completeRunnableListCallback(onEndCallback, mLauncher);
options.setOnAnimationAbortListener(endCallback);
options.setOnAnimationFinishedListener(endCallback);
options.setLaunchCookie(StableViewInfo.toLaunchCookie(itemInfo));
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
index 40cfe92..a01846d 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
@@ -30,6 +30,7 @@
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.quickstep.SystemUiProxy
import com.android.quickstep.TaskViewUtils
+import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled
import com.android.quickstep.views.DesktopTaskView
import com.android.quickstep.views.TaskContainer
import com.android.quickstep.views.TaskView
@@ -60,7 +61,11 @@
callback,
)
val transition = RemoteTransition(animRunner, appThread, "RecentsToDesktop")
- systemUiProxy.showDesktopApps(desktopTaskView.displayId, transition)
+ if (areMultiDesksFlagsEnabled()) {
+ systemUiProxy.activateDesk(desktopTaskView.deskId, transition)
+ } else {
+ systemUiProxy.showDesktopApps(desktopTaskView.displayId, transition)
+ }
}
/** Launch desktop tasks from recents view */
diff --git a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
index f9cec82..ada7301 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
@@ -16,7 +16,6 @@
package com.android.launcher3.model;
-import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -202,19 +201,12 @@
* Converts the list of {@link WidgetItem}s to the list of {@link ItemInfo}s.
*/
private List<ItemInfo> mapWidgetItemsToItemInfo(List<WidgetItem> widgetItems) {
- List<ItemInfo> items;
- if (enableCategorizedWidgetSuggestions()) {
- WidgetRecommendationCategoryProvider categoryProvider =
- new WidgetRecommendationCategoryProvider();
- items = widgetItems.stream()
- .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
- categoryProvider.getWidgetRecommendationCategory(mContext, it)))
- .collect(Collectors.toList());
- } else {
- items = widgetItems.stream().map(it -> new PendingAddWidgetInfo(it.widgetInfo,
- CONTAINER_WIDGETS_PREDICTION)).collect(Collectors.toList());
- }
- return items;
+ WidgetRecommendationCategoryProvider categoryProvider =
+ new WidgetRecommendationCategoryProvider();
+ return widgetItems.stream()
+ .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
+ categoryProvider.getWidgetRecommendationCategory(mContext, it)))
+ .collect(Collectors.toList());
}
/** Cleans up any open prediction sessions. */
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
index b732cba..0a4b7c8 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -15,7 +15,6 @@
*/
package com.android.launcher3.model;
-import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER;
@@ -129,20 +128,12 @@
}
}
- List<ItemInfo> items;
- if (enableCategorizedWidgetSuggestions()) {
- WidgetRecommendationCategoryProvider categoryProvider =
- new WidgetRecommendationCategoryProvider();
- items = servicePredictedItems.stream()
- .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
- categoryProvider.getWidgetRecommendationCategory(context, it)))
- .collect(Collectors.toList());
- } else {
- items = servicePredictedItems.stream()
- .map(it -> new PendingAddWidgetInfo(it.widgetInfo,
- CONTAINER_WIDGETS_PREDICTION)).collect(
- Collectors.toList());
- }
+ WidgetRecommendationCategoryProvider categoryProvider =
+ new WidgetRecommendationCategoryProvider();
+ List<ItemInfo> items = servicePredictedItems.stream()
+ .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
+ categoryProvider.getWidgetRecommendationCategory(context, it)))
+ .collect(Collectors.toList());
FixedContainerItems fixedContainerItems =
new FixedContainerItems(mPredictorState.containerId, items);
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
index eb24df1..2402a28 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -401,6 +401,39 @@
DisplayController.INSTANCE.get(context).notifyConfigChange()
}
+ private fun notifyOnDeskAdded(displayId: Int, deskId: Int) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyOnDeskAdded: displayId=$displayId, deskId=$deskId")
+ }
+
+ for (listener in desktopVisibilityListeners) {
+ listener.onDeskAdded(displayId, deskId)
+ }
+ }
+
+ private fun notifyOnDeskRemoved(displayId: Int, deskId: Int) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyOnDeskRemoved: displayId=$displayId, deskId=$deskId")
+ }
+
+ for (listener in desktopVisibilityListeners) {
+ listener.onDeskRemoved(displayId, deskId)
+ }
+ }
+
+ private fun notifyOnActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "notifyOnActiveDeskChanged: displayId=$displayId, newActiveDesk=$newActiveDesk, oldActiveDesk=$oldActiveDesk",
+ )
+ }
+
+ for (listener in desktopVisibilityListeners) {
+ listener.onActiveDeskChanged(displayId, newActiveDesk, oldActiveDesk)
+ }
+ }
+
/** TODO: b/333533253 - Remove after flag rollout */
private fun setBackgroundStateEnabled(backgroundStateEnabled: Boolean) {
if (DEBUG) {
@@ -511,6 +544,8 @@
"Found a duplicate desk Id: $deskId on display: $displayId"
}
}
+
+ notifyOnDeskAdded(displayId, deskId)
}
private fun onDeskRemoved(displayId: Int, deskId: Int) {
@@ -526,6 +561,8 @@
it.activeDeskId = INACTIVE_DESK_ID
}
}
+
+ notifyOnDeskRemoved(displayId, deskId)
}
private fun onActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) {
@@ -539,12 +576,16 @@
check(oldActiveDesk == it.activeDeskId) {
"Mismatch between the Shell's oldActiveDesk: $oldActiveDesk, and Launcher's: ${it.activeDeskId}"
}
- check(it.deskIds.contains(newActiveDesk)) {
+ check(newActiveDesk == INACTIVE_DESK_ID || it.deskIds.contains(newActiveDesk)) {
"newActiveDesk: $newActiveDesk was never added to display: $displayId"
}
it.activeDeskId = newActiveDesk
}
+ if (newActiveDesk != oldActiveDesk) {
+ notifyOnActiveDeskChanged(displayId, newActiveDesk, oldActiveDesk)
+ }
+
if (wasInDesktopMode != isInDesktopModeAndNotInOverview(displayId)) {
notifyIsInDesktopModeChanged(displayId, !wasInDesktopMode)
}
@@ -718,6 +759,6 @@
private const val TAG = "DesktopVisController"
private const val DEBUG = false
- public const val INACTIVE_DESK_ID = -1
+ const val INACTIVE_DESK_ID = -1
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index cc94824..1698050 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -170,7 +170,7 @@
mHasDesktopTask,
mWasDesktopTaskFilteredOut);
}, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER
- : RecentsFilterState.getEmptyDesktopTaskFilter());
+ : RecentsFilterState.getDesktopTaskFilter());
}
mQuickSwitchViewController.updateLayoutForSurface(wasOpenedFromTaskbar,
@@ -232,7 +232,7 @@
mWasDesktopTaskFilteredOut,
wasOpenedFromTaskbar);
}, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER
- : RecentsFilterState.getEmptyDesktopTaskFilter());
+ : RecentsFilterState.getDesktopTaskFilter());
}
private boolean shouldExcludeTask(GroupTask task, Set<Integer> taskIdsToExclude) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index f80dc90..15be03a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -40,6 +40,8 @@
import com.android.quickstep.util.BorderAnimator;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.wm.shell.shared.TypefaceUtils;
+import com.android.wm.shell.shared.TypefaceUtils.FontFamily;
import kotlin.Unit;
@@ -64,6 +66,11 @@
@Nullable private ImageView mIcon2;
@Nullable private View mContent;
+ // Describe the task position in the parent container. Used to add information about the task's
+ // position in a task list to the task view's content description.
+ private int mIndexInParent = -1;
+ private int mTotalTasksInParent = -1;
+
public KeyboardQuickSwitchTaskView(@NonNull Context context) {
this(context, null);
}
@@ -108,11 +115,11 @@
TypefaceUtils.setTypeface(
mContent.findViewById(R.id.large_text),
- TypefaceUtils.FONT_FAMILY_HEADLINE_LARGE_EMPHASIZED
+ FontFamily.GSF_HEADLINE_LARGE_EMPHASIZED
);
TypefaceUtils.setTypeface(
mContent.findViewById(R.id.small_text),
- TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE
+ FontFamily.GSF_LABEL_LARGE
);
Resources resources = mContext.getResources();
@@ -153,36 +160,51 @@
applyThumbnail(mThumbnailView1, task1, thumbnailUpdateFunction);
applyThumbnail(mThumbnailView2, task2, thumbnailUpdateFunction);
+ // Update content description, even in cases task icons, and content descriptions need to be
+ // loaded asynchronously to ensure that the task has non empty description (assuming task
+ // position information was set), as KeyboardQuickSwitch view may request accessibility
+ // focus to be moved to the task when the quick switch UI gets shown. The description will
+ // be updated once the task metadata has been loaded - the delay should be very short, and
+ // the content description when task titles are not available still gives some useful
+ // information to the user (the task's position in the list).
+ updateContentDesctiptionForTasks(task1, task2);
+
if (iconUpdateFunction == null) {
applyIcon(mIcon1, task1);
applyIcon(mIcon2, task2);
- setContentDescription(task2 == null
- ? task1.titleDescription
- : getContext().getString(
- R.string.quick_switch_split_task,
- task1.titleDescription,
- task2.titleDescription));
return;
}
+
iconUpdateFunction.updateIconInBackground(task1, t -> {
applyIcon(mIcon1, task1);
if (task2 != null) {
return;
}
- setContentDescription(task1.titleDescription);
+ updateContentDesctiptionForTasks(task1, null);
});
+
if (task2 == null) {
return;
}
iconUpdateFunction.updateIconInBackground(task2, t -> {
applyIcon(mIcon2, task2);
- setContentDescription(getContext().getString(
- R.string.quick_switch_split_task,
- task1.titleDescription,
- task2.titleDescription));
+ updateContentDesctiptionForTasks(task1, task2);
});
}
+ /**
+ * Initializes information about the task's position within the parent container context - used
+ * to add position information to the view's content description.
+ * Should be called before associating the view with tasks.
+ *
+ * @param index The view's 0-based index within the parent task container.
+ * @param totalTasks The total number of tasks in the parent task container.
+ */
+ protected void setPositionInformation(int index, int totalTasks) {
+ mIndexInParent = index;
+ mTotalTasksInParent = totalTasks;
+ }
+
protected void setThumbnailsForSplitTasks(
@NonNull Task task1,
@Nullable Task task2,
@@ -281,6 +303,28 @@
constantState.newDrawable(getResources(), getContext().getTheme()));
}
+ /**
+ * Updates the task view's content description to reflect tasks represented by the view.
+ */
+ private void updateContentDesctiptionForTasks(@NonNull Task task1, @Nullable Task task2) {
+ String tasksDescription = task1.titleDescription == null || task2 == null
+ ? task1.titleDescription
+ : getContext().getString(
+ R.string.quick_switch_split_task,
+ task1.titleDescription,
+ task2.titleDescription);
+ if (mIndexInParent < 0) {
+ setContentDescription(tasksDescription);
+ return;
+ }
+
+ setContentDescription(
+ getContext().getString(R.string.quick_switch_task_with_position_in_parent,
+ tasksDescription != null ? tasksDescription : "",
+ mIndexInParent + 1,
+ mTotalTasksInParent));
+ }
+
protected interface ThumbnailUpdateFunction {
void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback);
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 336ef48..ab147bb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -58,6 +58,8 @@
import com.android.quickstep.util.SplitTask;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+import com.android.wm.shell.shared.TypefaceUtils;
+import com.android.wm.shell.shared.TypefaceUtils.FontFamily;
import java.util.HashMap;
import java.util.List;
@@ -189,10 +191,9 @@
}
}
-
TypefaceUtils.setTypeface(
mNoRecentItemsPane.findViewById(R.id.no_recent_items_text),
- TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE);
+ FontFamily.GSF_LABEL_LARGE);
}
private void registerOnBackInvokedCallback() {
@@ -300,6 +301,7 @@
continue;
}
+ currentTaskView.setPositionInformation(i, tasksToDisplay);
currentTaskView.setThumbnailsForSplitTasks(
task1,
task2,
@@ -547,6 +549,9 @@
ViewOutlineProvider outlineProvider = getOutlineProvider();
+ int defaultFocusedTaskIndex = Math.min(
+ getTaskCount() - 1,
+ currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride);
mOpenAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
@@ -600,9 +605,7 @@
});
}
- animateFocusMove(-1, Math.min(
- getTaskCount() - 1,
- currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride));
+ animateFocusMove(-1, defaultFocusedTaskIndex);
displayedContent.setVisibility(VISIBLE);
setVisibility(VISIBLE);
requestFocus();
@@ -622,6 +625,11 @@
invalidateOutline();
mOpenAnimation = null;
InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
+
+ View focusedTask = getTaskAt(defaultFocusedTaskIndex);
+ if (focusedTask != null) {
+ focusedTask.requestAccessibilityFocus();
+ }
}
});
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 2272d11..62f546b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -135,11 +135,11 @@
@Override
protected void onDestroy() {
onLauncherVisibilityChanged(false /* isVisible */, true /* fromInitOrDestroy */);
+ mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
super.onDestroy();
mTaskbarLauncherStateController.onDestroy();
mLauncher.setTaskbarUIController(null);
- mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
mHomeState.removeListener(mVisibilityChangeListener);
}
@@ -279,7 +279,10 @@
private void postAdjustHotseatForBubbleBar() {
Hotseat hotseat = mLauncher.getHotseat();
if (hotseat == null || !isBubbleBarVisible()) return;
- hotseat.post(() -> adjustHotseatForBubbleBar(isBubbleBarVisible()));
+ hotseat.post(() -> {
+ if (mControllers == null) return;
+ adjustHotseatForBubbleBar(isBubbleBarVisible());
+ });
}
private boolean isBubbleBarVisible() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 39f80c2..6afbebf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -926,7 +926,7 @@
options.setSplashScreenStyle(splashScreenStyle);
options.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
- IRemoteCallback endCallback = completeRunnableListCallback(callbacks);
+ IRemoteCallback endCallback = completeRunnableListCallback(callbacks, this);
options.setOnAnimationAbortListener(endCallback);
options.setOnAnimationFinishedListener(endCallback);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index 5d1288c..d624413 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -52,6 +52,8 @@
import com.android.launcher3.views.BaseDragLayer
import com.android.quickstep.util.ContextualSearchInvoker
import com.android.quickstep.util.LottieAnimationColorUtils
+import com.android.wm.shell.shared.TypefaceUtils
+import com.android.wm.shell.shared.TypefaceUtils.FontFamily
import java.io.PrintWriter
/** First EDU step for swiping up to show transient Taskbar. */
@@ -164,7 +166,7 @@
tooltip?.run {
TypefaceUtils.setTypeface(
requireViewById(R.id.taskbar_edu_title),
- TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+ FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
)
val swipeAnimation = requireViewById<LottieAnimationView>(R.id.swipe_animation)
swipeAnimation.supportLightTheme()
@@ -210,19 +212,19 @@
TypefaceUtils.setTypeface(
requireViewById(R.id.taskbar_edu_title),
- TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+ FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
)
TypefaceUtils.setTypeface(
requireViewById(R.id.splitscreen_text),
- TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+ FontFamily.GSF_BODY_MEDIUM,
)
TypefaceUtils.setTypeface(
requireViewById(R.id.suggestions_text),
- TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+ FontFamily.GSF_BODY_MEDIUM,
)
TypefaceUtils.setTypeface(
requireViewById(R.id.pinning_text),
- TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+ FontFamily.GSF_BODY_MEDIUM,
)
// Set up layout parameters.
@@ -275,11 +277,11 @@
allowTouchDismissal = true
TypefaceUtils.setTypeface(
requireViewById(R.id.taskbar_edu_title),
- TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+ FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
)
TypefaceUtils.setTypeface(
requireViewById(R.id.pinning_text),
- TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+ FontFamily.GSF_BODY_MEDIUM,
)
val pinningAnim =
@@ -336,9 +338,9 @@
TypefaceUtils.setTypeface(
requireViewById(R.id.taskbar_edu_title),
- TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+ FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
)
- TypefaceUtils.setTypeface(eduSubtitle, TypefaceUtils.FONT_FAMILY_BODY_SMALL_BASELINE)
+ TypefaceUtils.setTypeface(eduSubtitle, FontFamily.GSF_BODY_SMALL)
showDisclosureText(eduSubtitle)
updateLayoutParams<BaseDragLayer.LayoutParams> {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt b/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt
deleted file mode 100644
index e9c62d1..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.taskbar
-
-import android.graphics.Typeface
-import android.widget.TextView
-import com.android.launcher3.Flags
-
-/**
- * Helper util class to set pre-defined typefaces to textviews
- *
- * If the typeface font family is already defined here, you can just reuse it directly. Otherwise,
- * please define it here for future use. You do not need to define the font style. If you need
- * anything other than [Typeface.NORMAL], pass it inline when calling [setTypeface]
- */
-class TypefaceUtils {
-
- companion object {
- const val FONT_FAMILY_BODY_SMALL_BASELINE = "variable-body-small"
- const val FONT_FAMILY_BODY_MEDIUM_BASELINE = "variable-body-medium"
- const val FONT_FAMILY_BODY_LARGE_BASELINE = "variable-body-large"
- const val FONT_FAMILY_LABEL_LARGE_BASELINE = "variable-label-large"
- const val FONT_FAMILY_DISPLAY_SMALL_EMPHASIZED = "variable-display-small-emphasized"
- const val FONT_FAMILY_DISPLAY_MEDIUM_EMPHASIZED = "variable-display-medium-emphasized"
- const val FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED = "variable-headline-small-emphasized"
- const val FONT_FAMILY_HEADLINE_LARGE_EMPHASIZED = "variable-headline-large-emphasized"
-
- @JvmStatic
- @JvmOverloads
- fun setTypeface(
- textView: TextView?,
- fontFamilyName: String,
- fontStyle: Int = Typeface.NORMAL,
- ) {
- if (!Flags.expressiveThemeInTaskbarAndNavigation()) return
- textView?.typeface = Typeface.create(fontFamilyName, fontStyle)
- }
- }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 1a42d21..cd0a4f3 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1274,7 +1274,7 @@
options.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
- IRemoteCallback endCallback = completeRunnableListCallback(callbacks);
+ IRemoteCallback endCallback = completeRunnableListCallback(callbacks, this);
options.setOnAnimationAbortListener(endCallback);
options.setOnAnimationFinishedListener(endCallback);
return new ActivityOptionsWrapper(options, callbacks);
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index ac88e5a..cc5b2da 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -38,8 +38,10 @@
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.statehandlers.DesktopVisibilityController;
+import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.launcher3.util.window.WindowManagerProxy;
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.ExternalDisplaysKt;
import com.android.quickstep.util.GroupTask;
@@ -70,7 +72,10 @@
/**
* Manages the recent task list from the system, caching it as necessary.
*/
-public class RecentTasksList {
+// TODO: b/401602554 - Consider letting [DesktopTasksController] notify [RecentTasksController] of
+// desk changes to trigger [IRecentTasksListener.onRecentTasksChanged()], instead of implementing
+// [DesktopVisibilityListener].
+public class RecentTasksList implements WindowManagerProxy.DesktopVisibilityListener {
private static final TaskLoadResult INVALID_RESULT = new TaskLoadResult(-1, false, 0);
@@ -78,6 +83,7 @@
private final KeyguardManager mKeyguardManager;
private final LooperExecutor mMainThreadExecutor;
private final SystemUiProxy mSysUiProxy;
+ private final DesktopVisibilityController mDesktopVisibilityController;
// The list change id, increments as the task list changes in the system
private int mChangeId;
@@ -95,13 +101,16 @@
public RecentTasksList(Context context, LooperExecutor mainThreadExecutor,
KeyguardManager keyguardManager, SystemUiProxy sysUiProxy,
- TopTaskTracker topTaskTracker) {
+ TopTaskTracker topTaskTracker,
+ DesktopVisibilityController desktopVisibilityController,
+ DaggerSingletonTracker tracker) {
mContext = context;
mMainThreadExecutor = mainThreadExecutor;
mKeyguardManager = keyguardManager;
mChangeId = 1;
mSysUiProxy = sysUiProxy;
- sysUiProxy.registerRecentTasksListener(new IRecentTasksListener.Stub() {
+ mDesktopVisibilityController = desktopVisibilityController;
+ final IRecentTasksListener recentTasksListener = new IRecentTasksListener.Stub() {
@Override
public void onRecentTasksChanged() throws RemoteException {
mMainThreadExecutor.execute(RecentTasksList.this::onRecentTasksChanged);
@@ -147,7 +156,19 @@
topTaskTracker.onVisibleTasksChanged(visibleTasks);
});
}
- });
+ };
+
+ mSysUiProxy.registerRecentTasksListener(recentTasksListener);
+ tracker.addCloseable(
+ () -> mSysUiProxy.unregisterRecentTasksListener(recentTasksListener));
+
+ if (DesktopModeStatus.enableMultipleDesktops(mContext)) {
+ mDesktopVisibilityController.registerDesktopVisibilityListener(
+ this);
+ tracker.addCloseable(
+ () -> mDesktopVisibilityController.unregisterDesktopVisibilityListener(this));
+ }
+
// We may receive onRunningTaskAppeared events later for tasks which have already been
// included in the list returned by mSysUiProxy.getRunningTasks(), or may receive
// onRunningTaskVanished for tasks not included in the returned list. These cases will be
@@ -286,6 +307,27 @@
return mRunningTasks;
}
+ @Override
+ public void onDeskAdded(int displayId, int deskId) {
+ onRecentTasksChanged();
+ }
+
+ @Override
+ public void onDeskRemoved(int displayId, int deskId) {
+ onRecentTasksChanged();
+ }
+
+ @Override
+ public void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) {
+ // Should desk activation changes lead to the invalidation of the loaded tasks? The cases
+ // are:
+ // - Switching from one active desk to another.
+ // - Switching from out of a desk session into an active desk.
+ // - Switching from an active desk to a non-desk session.
+ // These changes don't affect the list of desks, nor their contents, so let's ignore them
+ // for now.
+ }
+
private void onRunningTaskAppeared(RunningTaskInfo taskInfo) {
// Make sure this task is not already in the list
for (RunningTaskInfo existingTask : mRunningTasks) {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
index cf7e499..0deb1ca 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -59,6 +59,10 @@
if (!DesktopModeStatus.canEnterDesktopMode(context)) {
return false;
}
+ // TODO: b/400866688 - Check if we need to update this such that for an empty desk, we
+ // receive a list of apps that contain only the Launcher and the `DesktopWallpaperActivity`
+ // and both are fullscreen windowing mode. A desk can also have transparent modals and
+ // immersive apps which may not have a "freeform" windowing mode.
for (RemoteAnimationTarget target : apps) {
if (target.windowConfiguration.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
return true;
diff --git a/quickstep/src/com/android/quickstep/RecentsFilterState.java b/quickstep/src/com/android/quickstep/RecentsFilterState.java
index c4b0f25..1808a97 100644
--- a/quickstep/src/com/android/quickstep/RecentsFilterState.java
+++ b/quickstep/src/com/android/quickstep/RecentsFilterState.java
@@ -18,6 +18,7 @@
import androidx.annotation.Nullable;
+import com.android.quickstep.util.DesksUtils;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.TaskViewType;
import com.android.systemui.shared.recents.model.Task;
@@ -117,37 +118,43 @@
* Returns a predicate for filtering out GroupTasks by package name.
*
* @param packageName package name to filter GroupTasks by
- * if null, Predicate filters out desktop tasks with no non-minimized tasks.
+ * if null, Predicate filters out desktop tasks with no non-minimized tasks,
+ * unless the multiple desks feature is enabled, which allows empty desks.
*/
public static Predicate<GroupTask> getFilter(@Nullable String packageName) {
if (packageName == null) {
- return getEmptyDesktopTaskFilter();
+ return getDesktopTaskFilter();
}
return (groupTask) -> (groupTask.containsPackage(packageName)
- && !isDestopTaskWithMinimizedTasksOnly(groupTask));
+ && shouldKeepGroupTask(groupTask));
}
/**
- * Returns a predicate that filters out desk tasks that contain no non-minimized desktop tasks.
+ * Returns a predicate that filters out desk tasks that contain no non-minimized desktop tasks,
+ * unless the multiple desks feature is enabled, which allows empty desks.
*/
- public static Predicate<GroupTask> getEmptyDesktopTaskFilter() {
- return (groupTask -> !isDestopTaskWithMinimizedTasksOnly(groupTask));
+ public static Predicate<GroupTask> getDesktopTaskFilter() {
+ return (groupTask -> shouldKeepGroupTask(groupTask));
}
/**
- * Whether the provided task is a desktop task with no non-minimized tasks - returns true if the
- * desktop task has no tasks at all.
+ * Returns true if the given `groupTask` should be kept, and false if it should be filtered out.
+ * Desks will be filtered out if they are empty unless the multiple desks feature is enabled.
*
* @param groupTask The group task to check.
*/
- static boolean isDestopTaskWithMinimizedTasksOnly(GroupTask groupTask) {
+ private static boolean shouldKeepGroupTask(GroupTask groupTask) {
if (groupTask.taskViewType != TaskViewType.DESKTOP) {
- return false;
+ return true;
}
+
+ if (DesksUtils.areMultiDesksFlagsEnabled()) {
+ return true;
+ }
+
return groupTask.getTasks().stream()
- .filter(task -> !task.isMinimized)
- .toList().isEmpty();
+ .anyMatch(task -> !task.isMinimized);
}
/**
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 1d83d42..e1adf3d 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -43,6 +43,7 @@
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.util.DaggerSingletonObject;
import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.DisplayController;
@@ -61,6 +62,8 @@
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
+import dagger.Lazy;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -72,8 +75,6 @@
import javax.inject.Inject;
-import dagger.Lazy;
-
/**
* Singleton class to load and manage recents model.
*/
@@ -104,12 +105,14 @@
DisplayController displayController,
LockedUserState lockedUserState,
Lazy<ThemeManager> themeManagerLazy,
+ DesktopVisibilityController desktopVisibilityController,
DaggerSingletonTracker tracker
) {
// Lazily inject the ThemeManager and access themeManager once the device is
// unlocked. See b/393248495 for details.
this(context, new IconProvider(context), systemUiProxy, topTaskTracker,
- displayController, lockedUserState,themeManagerLazy, tracker);
+ displayController, lockedUserState, themeManagerLazy, desktopVisibilityController,
+ tracker);
}
@SuppressLint("VisibleForTests")
@@ -120,6 +123,7 @@
DisplayController displayController,
LockedUserState lockedUserState,
Lazy<ThemeManager> themeManagerLazy,
+ DesktopVisibilityController desktopVisibilityController,
DaggerSingletonTracker tracker) {
this(context,
new RecentTasksList(
@@ -127,7 +131,7 @@
MAIN_EXECUTOR,
context.getSystemService(KeyguardManager.class),
systemUiProxy,
- topTaskTracker),
+ topTaskTracker, desktopVisibilityController, tracker),
new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider, displayController),
new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR),
iconProvider,
@@ -205,7 +209,7 @@
@Override
public int getTasks(@Nullable Consumer<List<GroupTask>> callback) {
return mTaskList.getTasks(false /* loadKeysOnly */, callback,
- RecentsFilterState.getEmptyDesktopTaskFilter());
+ RecentsFilterState.getDesktopTaskFilter());
}
/**
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.kt b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
index d6f6540..506f85d 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.kt
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
@@ -1096,18 +1096,26 @@
// Desktop Mode
//
/** Calls shell to create a new desk (if possible) on the display whose ID is `displayId`. */
- fun createDesktop(displayId: Int) =
+ fun createDesk(displayId: Int) =
executeWithErrorLog({ "Failed call createDesk" }) { desktopMode?.createDesk(displayId) }
/**
* Calls shell to activate the desk whose ID is `deskId` on whatever display it exists on. This
* will bring all tasks on this desk to the front.
*/
- fun activateDesktop(deskId: Int, transition: RemoteTransition?) =
+ fun activateDesk(deskId: Int, transition: RemoteTransition?) =
executeWithErrorLog({ "Failed call activateDesk" }) {
desktopMode?.activateDesk(deskId, transition)
}
+ /** Calls shell to remove the desk whose ID is `deskId`. */
+ fun removeDesk(deskId: Int) =
+ executeWithErrorLog({ "Failed call removeDesk" }) { desktopMode?.removeDesk(deskId) }
+
+ /** Calls shell to remove all the available desks on all displays. */
+ fun removeAllDesks() =
+ executeWithErrorLog({ "Failed call removeAllDesks" }) { desktopMode?.removeAllDesks() }
+
/** Call shell to show all apps active on the desktop */
fun showDesktopApps(displayId: Int, transition: RemoteTransition?) =
executeWithErrorLog({ "Failed call showDesktopApps" }) {
@@ -1159,9 +1167,9 @@
}
/** Call shell to remove the desktop that is on given `displayId` */
- fun removeDesktop(displayId: Int) =
- executeWithErrorLog({ "Failed call removeDesktop" }) {
- desktopMode?.removeDesktop(displayId)
+ fun removeDefaultDeskInDisplay(displayId: Int) =
+ executeWithErrorLog({ "Failed call removeDefaultDeskInDisplay" }) {
+ desktopMode?.removeDefaultDeskInDisplay(displayId)
}
/** Call shell to move a task with given `taskId` to external display. */
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 1e61967..5995ca2 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -56,11 +56,12 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
-import com.android.launcher3.taskbar.TypefaceUtils;
import com.android.launcher3.views.ClipIconView;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
import com.android.systemui.shared.system.QuickStepContract;
+import com.android.wm.shell.shared.TypefaceUtils;
+import com.android.wm.shell.shared.TypefaceUtils.FontFamily;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieComposition;
@@ -409,7 +410,7 @@
if (mTutorialFragment.isAtFinalStep()) {
TypefaceUtils.setTypeface(
mDoneButton,
- TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE
+ FontFamily.GSF_LABEL_LARGE
);
showActionButton();
}
@@ -517,11 +518,11 @@
TypefaceUtils.setTypeface(
mFeedbackTitleView,
mTutorialFragment.isLargeScreen()
- ? TypefaceUtils.FONT_FAMILY_DISPLAY_MEDIUM_EMPHASIZED
- : TypefaceUtils.FONT_FAMILY_DISPLAY_SMALL_EMPHASIZED);
+ ? FontFamily.GSF_DISPLAY_MEDIUM_EMPHASIZED
+ : FontFamily.GSF_DISPLAY_SMALL_EMPHASIZED);
TypefaceUtils.setTypeface(
mFeedbackSubtitleView,
- TypefaceUtils.FONT_FAMILY_BODY_LARGE_BASELINE
+ FontFamily.GSF_BODY_LARGE
);
}
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index fda0c29..3492788 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -21,16 +21,22 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import android.animation.AnimatorSet;
-import android.annotation.NonNull;
+import android.os.BinderUtils;
import android.os.Bundle;
+import android.os.IBinder;
import android.os.IRemoteCallback;
import android.view.animation.Interpolator;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.views.ActivityContext;
import com.android.quickstep.views.RecentsViewContainer;
/**
@@ -95,14 +101,30 @@
}
/**
- * Returns a IRemoteCallback which completes the provided list as a result
+ * Returns a IRemoteCallback which completes the provided list as a result or when the owner
+ * is destroyed
*/
- public static IRemoteCallback completeRunnableListCallback(RunnableList list) {
+ public static IRemoteCallback completeRunnableListCallback(
+ RunnableList list, ActivityContext owner) {
+ DefaultLifecycleObserver destroyObserver = new DefaultLifecycleObserver() {
+ @Override
+ public void onDestroy(@NonNull LifecycleOwner owner) {
+ list.executeAllAndClear();
+ }
+ };
+ MAIN_EXECUTOR.execute(() -> owner.getLifecycle().addObserver(destroyObserver));
+ list.add(() -> owner.getLifecycle().removeObserver(destroyObserver));
+
return new IRemoteCallback.Stub() {
@Override
public void sendResult(Bundle bundle) {
MAIN_EXECUTOR.execute(list::executeAllAndDestroy);
}
+
+ @Override
+ public IBinder asBinder() {
+ return BinderUtils.wrapLifecycle(this, owner.getOwnerCleanupSet());
+ }
};
}
diff --git a/quickstep/src/com/android/quickstep/util/DesksUtils.kt b/quickstep/src/com/android/quickstep/util/DesksUtils.kt
new file mode 100644
index 0000000..ccfdbb9
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/DesksUtils.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.quickstep.util
+
+import android.content.Context
+import android.window.DesktopExperienceFlags
+import com.android.systemui.shared.recents.model.Task
+
+class DesksUtils {
+ companion object {
+ @JvmStatic
+ fun areMultiDesksFlagsEnabled() =
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue() &&
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_FRONTEND.isTrue()
+
+ /** Returns true if this [task] contains the [DesktopWallpaperActivity]. */
+ @JvmStatic
+ fun isDesktopWallpaperTask(context: Context, task: Task): Boolean {
+ val sysUiPackage =
+ context.getResources().getString(com.android.internal.R.string.config_systemUi)
+ val component = task.key.component
+ if (component != null) {
+ return component.className.contains("DesktopWallpaperActivity") &&
+ component.packageName.contains(sysUiPackage)
+ }
+ return false
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index 8954d80..9cdde01 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -24,6 +24,7 @@
import static com.android.launcher3.Flags.enableOverviewOnConnectedDisplays;
import static com.android.launcher3.LauncherPrefs.ALLOW_ROTATION;
+import static com.android.launcher3.LauncherPrefs.FIXED_LANDSCAPE_MODE;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.SettingsCache.ROTATION_SETTING_URI;
import static com.android.quickstep.BaseActivityInterface.getTaskDimension;
@@ -44,6 +45,7 @@
import androidx.annotation.NonNull;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherPrefChangeListener;
import com.android.launcher3.LauncherPrefs;
@@ -107,9 +109,12 @@
// Ignore shared prefs for home rotation rotation, allowing it in if the activity supports it
private static final int FLAG_IGNORE_ALLOW_HOME_ROTATION_PREF = 1 << 9;
+ // Shared prefs for fixed 90 degree rotation, activities should rotate if they support it
+ private static final int FLAG_HOME_FIXED_LANDSCAPE_PREFS = 1 << 10;
+
private static final int MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE =
FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY
- | FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY;
+ | FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY;
// State for which rotation watcher will be enabled. We skip it when home rotation or
// multi-window is enabled as in that case, activity itself rotates.
@@ -227,7 +232,7 @@
private boolean updateHandler() {
mRecentsActivityRotation = inferRecentsActivityRotation(mDisplayRotation);
- if (mRecentsActivityRotation == mTouchRotation || isRecentsActivityRotationAllowed()) {
+ if (mRecentsActivityRotation == mTouchRotation || shouldUseRealOrientation()) {
mOrientationHandler = RecentsPagedOrientationHandler.PORTRAIT;
} else if (mTouchRotation == ROTATION_90) {
mOrientationHandler = RecentsPagedOrientationHandler.LANDSCAPE;
@@ -249,9 +254,13 @@
return mStateId != oldStateId;
}
+ private boolean shouldUseRealOrientation() {
+ return isRecentsActivityRotationAllowed() || isLauncherFixedLandscape();
+ }
+
@SurfaceRotation
private int inferRecentsActivityRotation(@SurfaceRotation int displayRotation) {
- if (isRecentsActivityRotationAllowed()) {
+ if (shouldUseRealOrientation()) {
return mRecentsRotation < 0 ? displayRotation : mRecentsRotation;
} else {
return ROTATION_0;
@@ -288,6 +297,9 @@
if (LauncherPrefs.ALLOW_ROTATION.getSharedPrefKey().equals(s)) {
updateHomeRotationSetting();
}
+ if (LauncherPrefs.FIXED_LANDSCAPE_MODE.getSharedPrefKey().equals(s)) {
+ updateFixedLandscapeSetting();
+ }
}
private void updateAutoRotateSetting() {
@@ -295,6 +307,15 @@
mSettingsCache.getValue(ROTATION_SETTING_URI, 1));
}
+ private void updateFixedLandscapeSetting() {
+ if (Flags.oneGridSpecs()) {
+ setFlag(
+ FLAG_HOME_FIXED_LANDSCAPE_PREFS,
+ LauncherPrefs.get(mContext).get(FIXED_LANDSCAPE_MODE)
+ );
+ }
+ }
+
private void updateHomeRotationSetting() {
boolean homeRotationEnabled = LauncherPrefs.get(mContext).get(ALLOW_ROTATION);
setFlag(FLAG_HOME_ROTATION_ALLOWED_IN_PREFS, homeRotationEnabled);
@@ -307,6 +328,7 @@
// initialize external flags
updateAutoRotateSetting();
updateHomeRotationSetting();
+ updateFixedLandscapeSetting();
}
private void initMultipleOrientationListeners() {
@@ -383,15 +405,19 @@
setFlag(FLAG_IGNORE_ALLOW_HOME_ROTATION_PREF, true);
}
+ public boolean isLauncherFixedLandscape() {
+ return (mFlags & FLAG_HOME_FIXED_LANDSCAPE_PREFS) == FLAG_HOME_FIXED_LANDSCAPE_PREFS;
+ }
+
public boolean isRecentsActivityRotationAllowed() {
// Activity rotation is allowed if the multi-simulated-rotation is not supported
// (fallback recents or tablets) or activity rotation is enabled by various settings.
return ((mFlags & MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
!= MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
|| (mFlags & (FLAG_IGNORE_ALLOW_HOME_ROTATION_PREF
- | FLAG_HOME_ROTATION_ALLOWED_IN_PREFS
- | FLAG_MULTIWINDOW_ROTATION_ALLOWED
- | FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING)) != 0;
+ | FLAG_HOME_ROTATION_ALLOWED_IN_PREFS
+ | FLAG_MULTIWINDOW_ROTATION_ALLOWED
+ | FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING)) != 0;
}
/**
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 1d035e9..27657b4 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -35,6 +35,7 @@
import com.android.launcher3.Flags.enableOverviewIconMenu
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.R
+import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.launcher3.testing.TestLogging
import com.android.launcher3.testing.shared.TestProtocol
import com.android.launcher3.util.RunnableList
@@ -68,6 +69,8 @@
type = TaskViewType.DESKTOP,
thumbnailFullscreenParams = DesktopFullscreenDrawParams(context),
) {
+ var deskId = DesktopVisibilityController.INACTIVE_DESK_ID
+
private val contentViewFullscreenParams = FullscreenDrawParams(context)
private val taskContentViewPool =
@@ -281,6 +284,7 @@
orientedState: RecentsOrientedState,
taskOverlayFactory: TaskOverlayFactory,
) {
+ deskId = desktopTask.deskId
// TODO(b/370495260): Minimized tasks should not be filtered with desktop exploded view
// support.
// Minimized tasks should not be shown in Overview.
@@ -332,12 +336,18 @@
override fun onRecycle() {
super.onRecycle()
+ deskId = DesktopVisibilityController.INACTIVE_DESK_ID
explodeProgress = 0.0f
viewModel = null
visibility = VISIBLE
taskContainers.forEach { removeAndRecycleThumbnailView(it) }
}
+ override fun setOrientationState(orientationState: RecentsOrientedState) {
+ super.setOrientationState(orientationState)
+ iconView.setIconOrientation(orientationState, isGridTask)
+ }
+
@SuppressLint("RtlHardcoded")
override fun updateTaskSize(lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect) {
super.updateTaskSize(lastComputedTaskSize, lastComputedGridTaskSize)
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index 4a2be2a..5f08209 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -40,6 +40,8 @@
import com.android.launcher3.util.NavigationMode;
import com.android.quickstep.TaskOverlayFactory.OverlayUICallbacks;
import com.android.quickstep.util.LayoutUtils;
+import com.android.wm.shell.shared.TypefaceUtils;
+import com.android.wm.shell.shared.TypefaceUtils.FontFamily;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -157,6 +159,7 @@
// Currently, the only grouped task action is "save app pairs".
mActionButtons = findViewById(R.id.action_buttons);
mSaveAppPairButton = findViewById(R.id.action_save_app_pair);
+ TypefaceUtils.setTypeface(mSaveAppPairButton, FontFamily.GSF_LABEL_LARGE);
// Initialize a list to hold alphas for mActionButtons and any group action buttons.
mMultiValueAlphas[ACTIONS_ALPHAS] = new MultiValueAlpha(mActionButtons, NUM_ALPHAS);
mMultiValueAlphas[GROUP_ACTIONS_ALPHAS] =
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index f5d0bfe..bb2aa75 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -212,6 +212,7 @@
import com.android.quickstep.recents.viewmodel.RecentsViewModel;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.quickstep.util.AnimUtils;
+import com.android.quickstep.util.DesksUtils;
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LayoutUtils;
@@ -254,6 +255,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
@@ -1924,6 +1926,8 @@
return;
}
+ // TODO: b/400532675 - The use of `currentTaskIds`, `runningTaskIds`, and `focusedTaskIds`
+ // needs to be audited so that they can work with empty desks that have no tasks.
int[] currentTaskIds;
TaskView currentTaskView = getTaskViewAt(mCurrentPage);
if (currentTaskView != null) {
@@ -2826,9 +2830,13 @@
/**
* Called when a gesture from an app is starting.
*/
+ // TODO: b/401582344 - Implement a way to exclude the `DesktopWallpaperActivity` from being
+ // considered in Overview.
public void onGestureAnimationStart(Task[] runningTasks) {
Log.d(TAG, "onGestureAnimationStart - runningTasks: " + Arrays.toString(runningTasks));
mActiveGestureRunningTasks = runningTasks;
+
+
// This needs to be called before the other states are set since it can create the task view
if (mOrientationState.setGestureActive(true)) {
reapplyActiveRotation();
@@ -3062,6 +3070,21 @@
}
/**
+ * Creates a `DesktopTaskView` for the currently active desk on this display, which contains the
+ * gievn `runningTasks`.
+ */
+ private DesktopTaskView createDesktopTaskViewForActiveDesk(Task[] runningTasks) {
+ final int activeDeskId = mUtils.getActiveDeskIdOnThisDisplay();
+ final var desktopTaskView = (DesktopTaskView) getTaskViewFromPool(TaskViewType.DESKTOP);
+
+ // TODO: b/401582344 - Implement a way to exclude the `DesktopWallpaperActivity`.
+ desktopTaskView.bind(
+ new DesktopTask(activeDeskId, Arrays.asList(runningTasks)),
+ mOrientationState, mTaskOverlayFactory);
+ return desktopTaskView;
+ }
+
+ /**
* Creates a task view (if necessary) to represent the task with the {@param runningTaskId}.
*
* All subsequent calls to reload will keep the task as the first item until {@link #reset()}
@@ -3075,20 +3098,14 @@
}
int runningTaskViewId = -1;
- boolean needGroupTaskView = runningTasks.length > 1;
- boolean needDesktopTask = hasDesktopTask(runningTasks);
if (shouldAddStubTaskView(runningTasks)) {
boolean wasEmpty = getChildCount() == 0;
// Add an empty view for now until the task plan is loaded and applied
final TaskView taskView;
+ final boolean needGroupTaskView = runningTasks.length > 1;
+ final boolean needDesktopTask = hasDesktopTask(runningTasks);
if (needDesktopTask) {
- final int activeDeskId =
- DesktopVisibilityController.INSTANCE.get(mContext).getActiveDeskId(
- mContainer.getDisplay().getDisplayId());
- taskView = getTaskViewFromPool(TaskViewType.DESKTOP);
- ((DesktopTaskView) taskView).bind(
- new DesktopTask(activeDeskId, Arrays.asList(runningTasks)),
- mOrientationState, mTaskOverlayFactory);
+ taskView = createDesktopTaskViewForActiveDesk(runningTasks);
} else if (needGroupTaskView) {
taskView = getTaskViewFromPool(TaskViewType.GROUPED);
// When we create a placeholder task view mSplitBoundsConfig will be null, but with
@@ -3114,8 +3131,11 @@
measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
makeMeasureSpec(getMeasuredHeight(), EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
- } else if (getTaskViewByTaskId(runningTasks[0].key.id) != null) {
- runningTaskViewId = getTaskViewByTaskId(runningTasks[0].key.id).getTaskViewId();
+ } else {
+ var runningTaskView = getTaskViewByTaskId(runningTasks[0].key.id);
+ if (runningTaskView != null) {
+ runningTaskViewId = runningTaskView.getTaskViewId();
+ }
}
boolean runningTaskTileHidden = mRunningTaskTileHidden;
@@ -3154,6 +3174,10 @@
return true;
}
}
+
+ // A running empty desk will have a single running app for the `DesktopWallpaperActivity`.
+ // TODO: b/401582344 - Implement a way to exclude the `DesktopWallpaperActivity`.
+
return false;
}
@@ -4525,24 +4549,33 @@
return lastVisibleTaskView;
}
- private void removeTaskInternal(@NonNull TaskView dismissedTaskView) {
- UI_HELPER_EXECUTOR
- .getHandler()
- .post(
- () -> {
- if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()
- && dismissedTaskView instanceof DesktopTaskView) {
- // TODO: b/362720497 - Use the api with desktop id instead.
- SystemUiProxy.INSTANCE
+ private void removeTaskInternal(@NonNull TaskView dismissedTaskView) {
+ UI_HELPER_EXECUTOR
+ .getHandler()
+ .post(
+ () -> {
+ if (dismissedTaskView instanceof DesktopTaskView desktopTaskView) {
+ removeDesktopTaskView(desktopTaskView);
+ } else {
+ for (int taskId : dismissedTaskView.getTaskIds()) {
+ ActivityManagerWrapper.getInstance().removeTask(taskId);
+ }
+ }
+ });
+ }
+
+ private void removeDesktopTaskView(DesktopTaskView desktopTaskView) {
+ if (DesksUtils.areMultiDesksFlagsEnabled()) {
+ SystemUiProxy.INSTANCE
.get(getContext())
- .removeDesktop(mContainer.getDisplay().getDisplayId());
- } else {
- for (int taskId : dismissedTaskView.getTaskIds()) {
- ActivityManagerWrapper.getInstance().removeTask(taskId);
- }
- }
- });
- }
+ .removeDesk(desktopTaskView.getDeskId());
+ } else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
+ SystemUiProxy.INSTANCE
+ .get(getContext())
+ .removeDefaultDeskInDisplay(
+ mContainer.getDisplay().getDisplayId());
+ }
+ }
protected void onDismissAnimationEnds() {
AccessibilityManagerCompat.sendTestProtocolEventToTest(getContext(),
@@ -4562,6 +4595,12 @@
mPendingAnimation = anim;
mPendingAnimation.addEndListener(isSuccess -> {
if (isSuccess) {
+ // Remove desktops first, since desks can be empty (so they have no recent tasks),
+ // and closing all tasks on a desk doesn't always necessarily mean that the desk
+ // will be removed. So, there are no guarantees that the below call to
+ // `ActivityManagerWrapper::removeAllRecentTasks()` will be enough.
+ SystemUiProxy.INSTANCE.get(getContext()).removeAllDesks();
+
// Remove all the task views now
finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> {
UI_HELPER_EXECUTOR.getHandler().post(
@@ -4691,7 +4730,7 @@
private void createDesk(View view) {
SystemUiProxy.INSTANCE
.get(getContext())
- .createDesktop(mContainer.getDisplay().getDisplayId());
+ .createDesk(mContainer.getDisplay().getDisplayId());
}
@Override
@@ -6924,6 +6963,58 @@
// TODO: b/389209338 - update the AddDesktopButton's visibility on this.
}
+ @Override
+ public void onDeskAdded(int displayId, int deskId) {
+ // Ignore desk changes that don't belong to this display.
+ if (displayId != mContainer.getDisplay().getDisplayId()) {
+ return;
+ }
+
+ if (mUtils.getDesktopTaskViewForDeskId(deskId) != null) {
+ Log.e(TAG, "A task view for this desk has already been added.");
+ return;
+ }
+
+ // We assume that a newly added desk is always empty and gets added to the left of the
+ // `AddNewDesktopButton`.
+ DesktopTaskView desktopTaskView =
+ (DesktopTaskView) getTaskViewFromPool(TaskViewType.DESKTOP);
+ desktopTaskView.bind(new DesktopTask(deskId, new ArrayList<>()),
+ mOrientationState, mTaskOverlayFactory);
+
+ Objects.requireNonNull(mAddDesktopButton);
+ final int insertionIndex = 1 + indexOfChild(mAddDesktopButton);
+ addView(desktopTaskView, insertionIndex);
+
+ updateTaskSize();
+ updateChildTaskOrientations();
+
+ // TODO: b/401002178 - Recalculate the new current page such that the addition of the new
+ // desk does not result in a change in the current scroll page.
+ }
+
+ @Override
+ public void onDeskRemoved(int displayId, int deskId) {
+ // Ignore desk changes that don't belong to this display.
+ if (displayId != mContainer.getDisplay().getDisplayId()) {
+ return;
+ }
+
+ // We need to distinguish between desk removals that are triggered from outside of overview
+ // vs. the ones that were initiated from overview by dismissing the corresponding desktop
+ // task view.
+ var taskView = mUtils.getDesktopTaskViewForDeskId(deskId);
+ if (taskView != null) {
+ dismissTaskView(taskView, true, true);
+ }
+ }
+
+ @Override
+ public void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) {
+ // TODO: b/400870600 - We may need to add code here to special case when an empty desk gets
+ // activated, since `RemoteDesktopLaunchTransitionRunner` doesn't always get triggered.
+ }
+
/** 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 1c37986..037bef6 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -22,7 +22,11 @@
import androidx.core.view.children
import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks
+import com.android.launcher3.statehandlers.DesktopVisibilityController
+import com.android.launcher3.statehandlers.DesktopVisibilityController.Companion.INACTIVE_DESK_ID
import com.android.launcher3.util.IntArray
+import com.android.quickstep.util.DesksUtils
+import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
import com.android.quickstep.util.isExternalDisplay
import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
@@ -52,7 +56,12 @@
* @return Sorted list of GroupTasks to be used in the RecentsView.
*/
fun sortDesktopTasksToFront(tasks: List<GroupTask>): List<GroupTask> {
- val (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP }
+ var (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP }
+ if (DesksUtils.areMultiDesksFlagsEnabled()) {
+ // Desk IDs of newer desks are larger than those of older desks, hence we can use them
+ // to sort desks from old to new.
+ desktopTasks = desktopTasks.sortedBy { (it as DesktopTask).deskId }
+ }
return otherTasks + desktopTasks
}
@@ -114,6 +123,22 @@
it.isLargeTile && !(recentsView.isSplitSelectionActive && it is DesktopTaskView)
}
+ /**
+ * Returns the [DesktopTaskView] that matches the given [deskId], or null if it doesn't exist.
+ */
+ fun getDesktopTaskViewForDeskId(deskId: Int): DesktopTaskView? {
+ if (deskId == INACTIVE_DESK_ID) {
+ return null
+ }
+ return taskViews.firstOrNull { it is DesktopTaskView && it.deskId == deskId }
+ as? DesktopTaskView
+ }
+
+ /** Returns the active desk ID of the display that contains the [recentsView] instance. */
+ fun getActiveDeskIdOnThisDisplay(): Int =
+ DesktopVisibilityController.INSTANCE.get(recentsView.context)
+ .getActiveDeskId(recentsView.mContainer.display.displayId)
+
/** Returns the expected focus task. */
fun getFirstNonDesktopTaskView(): TaskView? =
if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index 0d80f6a..4cf1eae 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -39,10 +39,11 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StateManager;
import com.android.quickstep.util.AnimUtils;
import com.android.quickstep.util.SplitSelectStateController;
+import com.android.wm.shell.shared.TypefaceUtils;
+import com.android.wm.shell.shared.TypefaceUtils.FontFamily;
/**
* A rounded rectangular component containing a single TextView.
@@ -129,6 +130,7 @@
cancelTextView.setVisibility(VISIBLE);
cancelTextView.setOnClickListener((v) -> exitSplitSelection());
instructionTextView.setText(R.string.toast_contextual_split_select_app);
+ TypefaceUtils.setTypeface(instructionTextView, FontFamily.GSF_BODY_MEDIUM);
// After layout, expand touch target of cancel button to meet minimum a11y measurements.
post(() -> {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index b7f1d1d..55432b8 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -33,7 +33,6 @@
import android.view.Display
import android.view.MotionEvent
import android.view.View
-import android.view.View.OnClickListener
import android.view.ViewGroup
import android.view.ViewStub
import android.view.accessibility.AccessibilityNodeInfo
@@ -142,6 +141,7 @@
/** Returns whether the task is part of overview grid and not being focused. */
get() = container.deviceProfile.isTablet && !isLargeTile
+ // TODO: b/400532675 - This will not work for empty desks until b/400532675 is fixed.
val isRunningTask: Boolean
get() = this === recentsView?.runningTaskView
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
index 70bf6b4..9722e9d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
@@ -49,6 +49,8 @@
import androidx.test.filters.SmallTest;
import com.android.internal.R;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
+import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.LooperExecutor;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.TaskViewType;
@@ -102,7 +104,9 @@
.thenReturn(true);
mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor,
- mockKeyguardManager, mSystemUiProxy, mTopTaskTracker);
+ mockKeyguardManager, mSystemUiProxy, mTopTaskTracker,
+ mock(DesktopVisibilityController.class),
+ mock(DaggerSingletonTracker.class));
}
@Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentOrientedStateHandlerTests.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentOrientedStateHandlerTests.kt
new file mode 100644
index 0000000..3cdf608
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentOrientedStateHandlerTests.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.quickstep.util
+
+import android.view.Surface
+import android.view.Surface.ROTATION_0
+import android.view.Surface.ROTATION_180
+import android.view.Surface.ROTATION_90
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.quickstep.FallbackActivityInterface
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler.Companion.PORTRAIT
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+/**
+ * Test all possible inputs to RecentsOrientedState.updateHandler. It tests all possible
+ * combinations of rotations and relevant methods (two methods that return boolean values) but it
+ * only provides the expected result when the final rotation is different from ROTATION_0 for
+ * simplicity. So any case not shown in resultMap you can assume results in ROTATION_0.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RecentOrientedStateHandlerTests {
+
+ data class TestCase(
+ val recentsRotation: Int,
+ val displayRotation: Int,
+ val touchRotation: Int,
+ val isRotationAllowed: Boolean,
+ val isFixedLandscape: Boolean,
+ ) {
+ override fun toString(): String {
+ return "TestCase(recentsRotation=${Surface.rotationToString(recentsRotation)}, " +
+ "displayRotation=${Surface.rotationToString(displayRotation)}, " +
+ "touchRotation=${Surface.rotationToString(touchRotation)}, " +
+ "isRotationAllowed=$isRotationAllowed, " +
+ "isFixedLandscape=$isFixedLandscape)"
+ }
+ }
+
+ private fun runTestCase(testCase: TestCase, expectedHandler: RecentsPagedOrientationHandler) {
+ val recentOrientedState =
+ spy(
+ RecentsOrientedState(
+ ApplicationProvider.getApplicationContext(),
+ FallbackActivityInterface.INSTANCE,
+ ) {}
+ )
+ whenever(recentOrientedState.isRecentsActivityRotationAllowed).thenAnswer {
+ testCase.isRotationAllowed
+ }
+ whenever(recentOrientedState.isLauncherFixedLandscape).thenAnswer {
+ testCase.isFixedLandscape
+ }
+
+ recentOrientedState.update(testCase.displayRotation, testCase.touchRotation)
+ val rotation = recentOrientedState.orientationHandler.rotation
+ assertWithMessage("$testCase to ${Surface.rotationToString(rotation)},")
+ .that(rotation)
+ .isEqualTo(expectedHandler.rotation)
+ }
+
+ @Test
+ fun `test fixed landscape when device is portrait`() {
+ runTestCase(
+ TestCase(
+ recentsRotation = ROTATION_0,
+ displayRotation = -1,
+ touchRotation = ROTATION_0,
+ isRotationAllowed = false,
+ isFixedLandscape = true,
+ ),
+ PORTRAIT,
+ )
+ }
+
+ @Test
+ fun `test fixed landscape when device is landscape`() {
+ runTestCase(
+ TestCase(
+ recentsRotation = ROTATION_90,
+ displayRotation = -1,
+ touchRotation = ROTATION_0,
+ isRotationAllowed = false,
+ isFixedLandscape = true,
+ ),
+ PORTRAIT,
+ )
+ }
+
+ @Test
+ fun `test fixed landscape when device is seascape`() {
+ runTestCase(
+ TestCase(
+ recentsRotation = ROTATION_180,
+ displayRotation = -1,
+ touchRotation = ROTATION_0,
+ isRotationAllowed = false,
+ isFixedLandscape = true,
+ ),
+ PORTRAIT,
+ )
+ }
+
+ @Test
+ fun `test fixed landscape when device is portrait and display rotation is portrait`() {
+ runTestCase(
+ TestCase(
+ recentsRotation = ROTATION_0,
+ displayRotation = ROTATION_0,
+ touchRotation = ROTATION_0,
+ isRotationAllowed = false,
+ isFixedLandscape = true,
+ ),
+ PORTRAIT,
+ )
+ }
+
+ @Test
+ fun `test fixed landscape when device is landscape and display rotation is landscape `() {
+ runTestCase(
+ TestCase(
+ recentsRotation = ROTATION_90,
+ displayRotation = ROTATION_90,
+ touchRotation = ROTATION_0,
+ isRotationAllowed = false,
+ isFixedLandscape = true,
+ ),
+ PORTRAIT,
+ )
+ }
+
+ @Test
+ fun `test fixed landscape when device is seascape and display rotation is seascape`() {
+ runTestCase(
+ TestCase(
+ recentsRotation = ROTATION_180,
+ displayRotation = ROTATION_180,
+ touchRotation = ROTATION_0,
+ isRotationAllowed = false,
+ isFixedLandscape = true,
+ ),
+ PORTRAIT,
+ )
+ }
+}
diff --git a/src/android/os/BinderUtils.kt b/src/android/os/BinderUtils.kt
new file mode 100644
index 0000000..0536283
--- /dev/null
+++ b/src/android/os/BinderUtils.kt
@@ -0,0 +1,51 @@
+/*
+ * 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 android.os
+
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.WeakCleanupSet
+import com.android.launcher3.util.WeakCleanupSet.OnOwnerDestroyedCallback
+
+/** Utility methods related to Binder */
+object BinderUtils {
+
+ /** Creates a binder wrapper which is tied to the [lifecycle] */
+ @JvmStatic
+ fun <T : Binder> T.wrapLifecycle(cleanupSet: WeakCleanupSet): Binder =
+ LifecycleBinderWrapper(this, cleanupSet)
+
+ private class LifecycleBinderWrapper<T : Binder>(
+ private var realBinder: T?,
+ cleanupSet: WeakCleanupSet,
+ ) : Binder(realBinder?.interfaceDescriptor), OnOwnerDestroyedCallback {
+
+ init {
+ MAIN_EXECUTOR.execute { cleanupSet.addOnOwnerDestroyedCallback(this) }
+ }
+
+ override fun queryLocalInterface(descriptor: String): IInterface? =
+ realBinder?.queryLocalInterface(descriptor)
+
+ override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean =
+ realBinder?.transact(code, data, reply, flags)
+ ?: throw RemoteException("Original binder cleaned up")
+
+ override fun onOwnerDestroyed() {
+ realBinder = null
+ }
+ }
+}
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 3e6b4dd..2426a61 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -55,6 +55,7 @@
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.ViewCache;
+import com.android.launcher3.util.WeakCleanupSet;
import com.android.launcher3.util.WindowBounds;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.ScrimView;
@@ -105,6 +106,7 @@
private final SavedStateRegistryController mSavedStateRegistryController =
SavedStateRegistryController.create(this);
private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
+ private final WeakCleanupSet mCleanupSet = new WeakCleanupSet(this);
protected DeviceProfile mDeviceProfile;
protected SystemUiController mSystemUiController;
@@ -504,6 +506,11 @@
return mLifecycleRegistry;
}
+ @Override
+ public WeakCleanupSet getOwnerCleanupSet() {
+ return mCleanupSet;
+ }
+
public static <T extends BaseActivity> T fromContext(Context context) {
if (context instanceof BaseActivity) {
return (T) context;
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index cb3a0bc..9a9bc1d 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN;
import static com.android.launcher3.Flags.enableSmartspaceAsAWidget;
+import static com.android.launcher3.graphics.ShapeDelegate.DEFAULT_PATH_SIZE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
@@ -39,6 +40,7 @@
import android.graphics.LightingColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
+import android.graphics.Path;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -632,7 +634,7 @@
Drawable badge = null;
if ((info instanceof ItemInfoWithIcon iiwi) && !iiwi.getMatchingLookupFlag().useLowRes()) {
- badge = iiwi.bitmap.getBadgeDrawable(context, useTheme);
+ badge = iiwi.bitmap.getBadgeDrawable(context, useTheme, getIconShapeOrNull(context));
}
if (info instanceof PendingAddShortcutInfo) {
@@ -659,8 +661,11 @@
// Only fetch badge if the icon is on workspace
if (info.id != ItemInfo.NO_ID && badge == null) {
badge = appState.getIconCache().getShortcutInfoBadge(si).newIcon(
- context, ThemeManager.INSTANCE.get(context).isIconThemeEnabled()
- ? FLAG_THEMED : 0);
+ context,
+ ThemeManager.INSTANCE.get(context).isIconThemeEnabled()
+ ? FLAG_THEMED : 0,
+ getIconShapeOrNull(context)
+ );
}
}
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
@@ -706,10 +711,11 @@
if (badge == null) {
badge = BitmapInfo.LOW_RES_INFO.withFlags(
- UserCache.INSTANCE.get(context)
- .getUserInfo(info.user)
- .applyBitmapInfoFlags(FlagOp.NO_OP))
- .getBadgeDrawable(context, useTheme);
+ UserCache.INSTANCE.get(context)
+ .getUserInfo(info.user)
+ .applyBitmapInfoFlags(FlagOp.NO_OP)
+ )
+ .getBadgeDrawable(context, useTheme, getIconShapeOrNull(context));
if (badge == null) {
badge = new ColorDrawable(Color.TRANSPARENT);
}
@@ -939,4 +945,18 @@
}
return null;
}
+
+ /**
+ * Returns current icon shape to use for badges if flag is on, otherwise null.
+ */
+ @Nullable
+ public static Path getIconShapeOrNull(Context context) {
+ if (Flags.enableLauncherIconShapes()) {
+ return ThemeManager.INSTANCE.get(context)
+ .getIconShape()
+ .getPath(DEFAULT_PATH_SIZE);
+ } else {
+ return null;
+ }
+ }
}
diff --git a/src/com/android/launcher3/graphics/ShapeDelegate.kt b/src/com/android/launcher3/graphics/ShapeDelegate.kt
index 9033eac..7c04292 100644
--- a/src/com/android/launcher3/graphics/ShapeDelegate.kt
+++ b/src/com/android/launcher3/graphics/ShapeDelegate.kt
@@ -203,7 +203,11 @@
start =
poly.transformed(
Matrix().apply {
- setRectToRect(RectF(0f, 0f, 100f, 100f), RectF(startRect), FILL)
+ setRectToRect(
+ RectF(0f, 0f, DEFAULT_PATH_SIZE, DEFAULT_PATH_SIZE),
+ RectF(startRect),
+ FILL,
+ )
}
),
end =
@@ -281,7 +285,10 @@
PathParser.createPathFromPathData(shapeStr).apply {
transform(
Matrix().apply {
- setScale(AREA_CALC_SIZE / 100f, AREA_CALC_SIZE / 100f)
+ setScale(
+ AREA_CALC_SIZE / DEFAULT_PATH_SIZE,
+ AREA_CALC_SIZE / DEFAULT_PATH_SIZE,
+ )
}
)
}
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index b60b8cc..f5e5e16 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -26,6 +26,7 @@
import androidx.annotation.Nullable;
import com.android.launcher3.Flags;
+import com.android.launcher3.Utilities;
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.BitmapInfo.DrawableCreationFlags;
@@ -325,10 +326,12 @@
* Returns a FastBitmapDrawable with the icon and context theme applied
*/
public FastBitmapDrawable newIcon(Context context, @DrawableCreationFlags int creationFlags) {
- if (!ThemeManager.INSTANCE.get(context).isIconThemeEnabled()) {
+ ThemeManager themeManager = ThemeManager.INSTANCE.get(context);
+ if (!themeManager.isIconThemeEnabled()) {
creationFlags &= ~FLAG_THEMED;
}
- FastBitmapDrawable drawable = bitmap.newIcon(context, creationFlags);
+ FastBitmapDrawable drawable = bitmap.newIcon(
+ context, creationFlags, Utilities.getIconShapeOrNull(context));
drawable.setIsDisabled(isDisabled());
return drawable;
}
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 20c0ecc..98a3882 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -219,6 +219,6 @@
public static UserBadgeDrawable getBadgeDrawable(Context context, UserHandle userHandle) {
return (UserBadgeDrawable) BitmapInfo.LOW_RES_INFO.withFlags(UserCache.getInstance(context)
.getUserInfo(userHandle).applyBitmapInfoFlags(FlagOp.NO_OP))
- .getBadgeDrawable(context, false /* isThemed */);
+ .getBadgeDrawable(context, false /* isThemed */, null);
}
}
diff --git a/src/com/android/launcher3/util/BaseContext.kt b/src/com/android/launcher3/util/BaseContext.kt
index 819470b..8aa10f3 100644
--- a/src/com/android/launcher3/util/BaseContext.kt
+++ b/src/com/android/launcher3/util/BaseContext.kt
@@ -49,6 +49,7 @@
private val savedStateRegistryController = SavedStateRegistryController.create(this)
private val lifecycleRegistry = LifecycleRegistry(this)
+ private val cleanupSet = WeakCleanupSet(this)
override val savedStateRegistry: SavedStateRegistry
get() = savedStateRegistryController.savedStateRegistry
@@ -110,6 +111,8 @@
override fun getViewCache() = viewCache
+ override fun getOwnerCleanupSet() = cleanupSet
+
private fun updateState() {
if (lifecycleRegistry.currentState.isAtLeast(CREATED)) {
lifecycleRegistry.currentState =
diff --git a/src/com/android/launcher3/util/WeakCleanupSet.kt b/src/com/android/launcher3/util/WeakCleanupSet.kt
new file mode 100644
index 0000000..7bf3289
--- /dev/null
+++ b/src/com/android/launcher3/util/WeakCleanupSet.kt
@@ -0,0 +1,56 @@
+/*
+ * 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 androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import java.util.Collections
+import java.util.WeakHashMap
+
+/**
+ * Utility class which maintains a list of cleanup callbacks using weak-references. These callbacks
+ * are called when the [owner] is destroyed, but can also be cleared when the caller is GCed
+ */
+class WeakCleanupSet(owner: LifecycleOwner) {
+
+ private val callbacks = Collections.newSetFromMap<OnOwnerDestroyedCallback>(WeakHashMap())
+ private var destroyed = false
+
+ init {
+ MAIN_EXECUTOR.execute {
+ owner.lifecycle.addObserver(
+ object : DefaultLifecycleObserver {
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ destroyed = true
+ callbacks.forEach { it.onOwnerDestroyed() }
+ }
+ }
+ )
+ }
+ }
+
+ fun addOnOwnerDestroyedCallback(callback: OnOwnerDestroyedCallback) {
+ if (destroyed) callback.onOwnerDestroyed() else callbacks.add(callback)
+ }
+
+ /** Callback when the owner is destroyed */
+ interface OnOwnerDestroyedCallback {
+ fun onOwnerDestroyed()
+ }
+}
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 11f0bc2..68e0324 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -520,6 +520,33 @@
*/
default void onCanCreateDesksChanged(boolean canCreateDesks) {
}
+
+ /**
+ * Called when a new desk is added.
+ *
+ * @param displayId The ID of the display on which the desk was added.
+ * @param deskId The ID of the newly added desk.
+ */
+ default void onDeskAdded(int displayId, int deskId) {}
+
+ /**
+ * Called when an existing desk is removed.
+ *
+ * @param displayId The ID of the display on which the desk was removed.
+ * @param deskId The ID of the desk that was removed.
+ */
+ default void onDeskRemoved(int displayId, int deskId) {}
+
+ /**
+ * Called when the active desk changes.
+ *
+ * @param displayId The ID of the display on which the desk activation change is happening.
+ * @param newActiveDesk The ID of the new active desk or -1 if no desk is active anymore
+ * (i.e. exit desktop mode).
+ * @param oldActiveDesk The ID of the desk that was previously active, or -1 if no desk was
+ * active before.
+ */
+ default void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) {}
}
}
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index bcb9295..cbf5341 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -87,6 +87,7 @@
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.ViewCache;
+import com.android.launcher3.util.WeakCleanupSet;
import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
import java.util.List;
@@ -520,6 +521,9 @@
return new CellPosMapper(dp.isVerticalBarLayout(), dp.numShownHotseatIcons);
}
+ /** Set to manage objects that can be cleaned up along with the context */
+ WeakCleanupSet getOwnerCleanupSet();
+
/** Whether bubbles are enabled. */
default boolean isBubbleBarEnabled() {
return false;
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index fda5175..af31276 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -16,7 +16,6 @@
package com.android.launcher3.widget;
import static com.android.app.animation.Interpolators.EMPHASIZED;
-import static com.android.launcher3.Flags.enableWidgetTapToAdd;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP;
@@ -150,40 +149,36 @@
return;
}
- if (enableWidgetTapToAdd()) {
- scrollToWidgetCell(wc);
+ scrollToWidgetCell(wc);
- if (mWidgetCellWithAddButton != null) {
- if (mWidgetCellWithAddButton.isShowingAddButton()) {
- // If there is a add button currently showing, hide it.
- mWidgetCellWithAddButton.hideAddButton(/* animate= */ true);
- } else {
- // The last recorded widget cell to show an add button is no longer showing it,
- // likely because the widget cell has been recycled or lost focus. If this is
- // the cell that has been clicked, we will show it below.
- mWidgetCellWithAddButton = null;
- }
- }
-
- if (mWidgetCellWithAddButton != wc) {
- // If click is on a cell not showing an add button, show it now.
- final PendingAddItemInfo info = (PendingAddItemInfo) wc.getTag();
- if (mActivityContext instanceof Launcher) {
- wc.showAddButton((view) -> addWidget(info));
- } else {
- wc.showAddButton((view) -> mActivityContext.getItemOnClickListener()
- .onClick(wc));
- }
- }
-
- mWidgetCellWithAddButton = mWidgetCellWithAddButton != wc ? wc : null;
- if (mWidgetCellWithAddButton != null) {
- mLastSelectedWidgetItem = mWidgetCellWithAddButton.getWidgetItem();
+ if (mWidgetCellWithAddButton != null) {
+ if (mWidgetCellWithAddButton.isShowingAddButton()) {
+ // If there is a add button currently showing, hide it.
+ mWidgetCellWithAddButton.hideAddButton(/* animate= */ true);
} else {
- mLastSelectedWidgetItem = null;
+ // The last recorded widget cell to show an add button is no longer showing it,
+ // likely because the widget cell has been recycled or lost focus. If this is
+ // the cell that has been clicked, we will show it below.
+ mWidgetCellWithAddButton = null;
}
+ }
+
+ if (mWidgetCellWithAddButton != wc) {
+ // If click is on a cell not showing an add button, show it now.
+ final PendingAddItemInfo info = (PendingAddItemInfo) wc.getTag();
+ if (mActivityContext instanceof Launcher) {
+ wc.showAddButton((view) -> addWidget(info));
+ } else {
+ wc.showAddButton((view) -> mActivityContext.getItemOnClickListener()
+ .onClick(wc));
+ }
+ }
+
+ mWidgetCellWithAddButton = mWidgetCellWithAddButton != wc ? wc : null;
+ if (mWidgetCellWithAddButton != null) {
+ mLastSelectedWidgetItem = mWidgetCellWithAddButton.getWidgetItem();
} else {
- mActivityContext.getItemOnClickListener().onClick(wc);
+ mLastSelectedWidgetItem = null;
}
}
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 7a27bf4..130843b 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -18,7 +18,6 @@
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
-import static com.android.launcher3.Flags.enableWidgetTapToAdd;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
import static com.android.launcher3.widget.util.WidgetSizes.getWidgetItemSizePx;
@@ -152,24 +151,22 @@
mWidgetTextContainer = findViewById(R.id.widget_text_container);
mWidgetAddButton = findViewById(R.id.widget_add_button);
- if (enableWidgetTapToAdd()) {
- setAccessibilityDelegate(new AccessibilityDelegate() {
- @Override
- public void onInitializeAccessibilityNodeInfo(View host,
- AccessibilityNodeInfo info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
- if (hasOnClickListeners()) {
- String accessibilityLabel = getResources().getString(
- mWidgetAddButton.isShown()
- ? R.string.widget_cell_tap_to_hide_add_button_label
- : R.string.widget_cell_tap_to_show_add_button_label);
- info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK,
- accessibilityLabel));
- }
+ setAccessibilityDelegate(new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host,
+ AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ if (hasOnClickListeners()) {
+ String accessibilityLabel = getResources().getString(
+ mWidgetAddButton.isShown()
+ ? R.string.widget_cell_tap_to_hide_add_button_label
+ : R.string.widget_cell_tap_to_show_add_button_label);
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK,
+ accessibilityLabel));
}
- });
- mWidgetAddButton.setVisibility(INVISIBLE);
- }
+ }
+ });
+ mWidgetAddButton.setVisibility(INVISIBLE);
}
public void setRemoteViewsPreview(RemoteViews view) {
@@ -210,9 +207,7 @@
showDescription(true);
showDimensions(true);
- if (enableWidgetTapToAdd()) {
- hideAddButton(/* animate= */ false);
- }
+ hideAddButton(/* animate= */ false);
if (mActiveRequest != null) {
mActiveRequest.cancel();
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 7a218ae..b0abf23 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -15,9 +15,7 @@
*/
package com.android.launcher3.widget.picker;
-import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker;
-import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_EXPAND_PRESS;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
@@ -82,12 +80,10 @@
import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
-import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
@@ -632,35 +628,19 @@
if (mIsInSearchMode) {
return;
}
- if (enableCategorizedWidgetSuggestions()) {
- // We avoid applying new recommendations when some are already displayed.
- if (mRecommendedWidgetsMap.isEmpty()) {
- mRecommendedWidgetsMap =
- mActivityContext.getWidgetPickerDataProvider().get().getRecommendations();
- }
- mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
- mRecommendedWidgetsMap,
- mDeviceProfile,
- /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
- /* availableWidth= */ mMaxSpanPerRow,
- /* cellPadding= */ mWidgetCellHorizontalPadding,
- /* requestedPage= */ mRecommendationsCurrentPage
- );
- } else {
- if (mRecommendedWidgets.isEmpty()) {
- mRecommendedWidgets = mActivityContext.getWidgetPickerDataProvider().get()
- .getRecommendations()
- .values().stream()
- .flatMap(Collection::stream).collect(Collectors.toList());
- mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
- mRecommendedWidgets,
- mDeviceProfile,
- /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
- /* availableWidth= */ mMaxSpanPerRow,
- /* cellPadding= */ mWidgetCellHorizontalPadding
- );
- }
+ // We avoid applying new recommendations when some are already displayed.
+ if (mRecommendedWidgetsMap.isEmpty()) {
+ mRecommendedWidgetsMap =
+ mActivityContext.getWidgetPickerDataProvider().get().getRecommendations();
}
+ mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
+ mRecommendedWidgetsMap,
+ mDeviceProfile,
+ /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
+ /* availableWidth= */ mMaxSpanPerRow,
+ /* cellPadding= */ mWidgetCellHorizontalPadding,
+ /* requestedPage= */ mRecommendationsCurrentPage
+ );
mWidgetRecommendationsContainer.setVisibility(
mRecommendedWidgetsCount > 0 ? VISIBLE : GONE);
@@ -792,13 +772,7 @@
}
private static int getWidgetSheetId(BaseActivity activity) {
- boolean isTwoPane = (activity.getDeviceProfile().isTablet
- // Enables two pane picker for tablets in all orientations when the
- // enableCategorizedWidgetSuggestions flag is on.
- && (activity.getDeviceProfile().isLandscape || enableCategorizedWidgetSuggestions())
- && !activity.getDeviceProfile().isTwoPanels)
- // Enables two pane picker for unfolded foldables if the flag is on.
- || (activity.getDeviceProfile().isTwoPanels && enableUnfoldedTwoPanePicker());
+ boolean isTwoPane = activity.getDeviceProfile().isTablet;
return isTwoPane ? R.layout.widgets_two_pane_sheet : R.layout.widgets_full_sheet;
}
@@ -945,16 +919,7 @@
private static boolean shouldRecreateLayout(DeviceProfile oldDp, DeviceProfile newDp) {
// When folding/unfolding the foldables, we need to switch between the regular widget picker
// and the two pane picker, so we rebuild the picker with the correct layout.
- boolean isFoldUnFold =
- oldDp.isTwoPanels != newDp.isTwoPanels && enableUnfoldedTwoPanePicker();
- // In tablets, on orientation change we switch between single and two pane picker unless the
- // categorized suggestions flag was on. With the categorized suggestions feature, we use a
- // two pane picker across all orientations.
- boolean useDifferentLayoutOnOrientationChange =
- (!enableCategorizedWidgetSuggestions() && (newDp.isTablet && !newDp.isTwoPanels
- && oldDp.isLandscape != newDp.isLandscape));
-
- return isFoldUnFold || useDifferentLayoutOnOrientationChange;
+ return oldDp.isTwoPanels != newDp.isTwoPanels;
}
/**
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
index 216f4d4..9ee9150 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -15,7 +15,6 @@
*/
package com.android.launcher3.widget.picker;
-import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
import static com.android.launcher3.widget.util.WidgetSizes.getWidgetSizePx;
import static com.android.launcher3.widget.util.WidgetsTableUtils.WIDGETS_TABLE_ROW_COUNT_COMPARATOR;
@@ -112,10 +111,8 @@
WidgetCell widgetCell = addItemCell(tableRow);
widgetCell.applyFromCellItem(widgetItem);
widgetCell.showAppIconInWidgetTitle(true);
- if (enableCategorizedWidgetSuggestions()) {
- widgetCell.showDescription(false);
- widgetCell.showDimensions(false);
- }
+ widgetCell.showDescription(false);
+ widgetCell.showDimensions(false);
}
addView(tableRow);
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index df76400..9ffaf51 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -15,9 +15,7 @@
*/
package com.android.launcher3.widget.picker;
-import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker;
-import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER;
import static com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER;
import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
@@ -215,22 +213,9 @@
@Override
protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) {
- if (enableCategorizedWidgetSuggestions()) {
- // two pane picker is full width for fold as well as tablet.
- return getResources().getDimensionPixelSize(
- R.dimen.widget_picker_two_panels_left_right_margin);
- }
- if (deviceProfile.isTwoPanels && enableUnfoldedTwoPanePicker()) {
- // enableUnfoldedTwoPanePicker made two pane picker full-width for fold only.
- return getResources().getDimensionPixelSize(
- R.dimen.widget_picker_two_panels_left_right_margin);
- }
- if (deviceProfile.isLandscape && !deviceProfile.isTwoPanels) {
- // non-fold tablet landscape margins (ag/22163531)
- return getResources().getDimensionPixelSize(
- R.dimen.widget_picker_landscape_tablet_left_right_margin);
- }
- return deviceProfile.allAppsLeftRightMargin;
+ // two pane picker is full width for fold as well as tablet.
+ return getResources().getDimensionPixelSize(
+ R.dimen.widget_picker_two_panels_left_right_margin);
}
@Override
@@ -257,7 +242,7 @@
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
- if (changed && mDeviceProfile.isTwoPanels && enableUnfoldedTwoPanePicker()) {
+ if (changed && mDeviceProfile.isTwoPanels) {
LinearLayout layout = mContent.findViewById(R.id.linear_layout_container);
FrameLayout leftPane = layout.findViewById(R.id.recycler_view_container);
LinearLayout.LayoutParams layoutParams = (LayoutParams) leftPane.getLayoutParams();
@@ -427,7 +412,7 @@
protected int getAvailableWidthForSuggestions(int pickerAvailableWidth) {
int rightPaneWidth = (int) Math.ceil(0.67 * pickerAvailableWidth);
- if (mDeviceProfile.isTwoPanels && enableUnfoldedTwoPanePicker()) {
+ if (mDeviceProfile.isTwoPanels) {
// See onLayout
int leftPaneWidth = (int) (0.33 * pickerAvailableWidth);
@Px int minLeftPaneWidthPx = Utilities.dpToPx(MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP);
@@ -552,13 +537,9 @@
}
WidgetsListContentEntry contentEntryToBind;
- if (enableCategorizedWidgetSuggestions()) {
- // Setting max span size enables row to understand how to fit more than one item
- // in a row.
- contentEntryToBind = contentEntry.withMaxSpanSize(mMaxSpanPerRow);
- } else {
- contentEntryToBind = contentEntry;
- }
+ // Setting max span size enables row to understand how to fit more than one item
+ // in a row.
+ contentEntryToBind = contentEntry.withMaxSpanSize(mMaxSpanPerRow);
WidgetsRowViewHolder widgetsRowViewHolder =
mWidgetsListTableViewHolderBinder.newViewHolder(mRightPane);
diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt b/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt
index d611ae8..91ba628 100644
--- a/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt
@@ -34,19 +34,20 @@
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val canvas = mock<Canvas>()
private val systemUnderTest =
- UserBadgeDrawable(context, R.drawable.ic_work_app_badge, R.color.badge_tint_work, false)
+ UserBadgeDrawable(
+ context,
+ R.drawable.ic_work_app_badge,
+ R.color.badge_tint_work,
+ false /* isThemed */,
+ null, /* shape */
+ )
@Test
fun draw_opaque() {
val colorList = mutableListOf<Int>()
- whenever(
- canvas.drawCircle(
- any(),
- any(),
- any(),
- any()
- )
- ).then { colorList.add(it.getArgument<Paint>(3).color) }
+ whenever(canvas.drawCircle(any(), any(), any(), any())).then {
+ colorList.add(it.getArgument<Paint>(3).color)
+ }
systemUnderTest.alpha = 255
systemUnderTest.draw(canvas)
@@ -57,14 +58,9 @@
@Test
fun draw_transparent() {
val colorList = mutableListOf<Int>()
- whenever(
- canvas.drawCircle(
- any(),
- any(),
- any(),
- any()
- )
- ).then { colorList.add(it.getArgument<Paint>(3).color) }
+ whenever(canvas.drawCircle(any(), any(), any(), any())).then {
+ colorList.add(it.getArgument<Paint>(3).color)
+ }
systemUnderTest.alpha = 0
systemUnderTest.draw(canvas)
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 95d5076..f490bd6 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -36,7 +36,6 @@
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 org.junit.Test;
@@ -127,7 +126,6 @@
* Adds three icons to the workspace and removes one of them by dragging to uninstall.
*/
@Test
- @ScreenRecordRule.ScreenRecord // b/399756302
@PlatinumTest(focusArea = "launcher")
public void uninstallWorkspaceIcon() throws IOException {
Point[] gridPositions = TestUtil.getCornersAndCenterPositions(mLauncher);