Merge "Fix hotseat icon translation issue" into main
diff --git a/quickstep/res/values-mr/strings.xml b/quickstep/res/values-mr/strings.xml
index 7941156..b053a21 100644
--- a/quickstep/res/values-mr/strings.xml
+++ b/quickstep/res/values-mr/strings.xml
@@ -95,7 +95,7 @@
<string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"सिस्टीम नेव्हिगेशन सेटिंग्ज"</annotation></string>
<string name="action_share" msgid="2648470652637092375">"शेअर करा"</string>
<string name="action_screenshot" msgid="8171125848358142917">"स्क्रीनशॉट"</string>
- <string name="action_split" msgid="2098009717623550676">"स्प्लिट"</string>
+ <string name="action_split" msgid="2098009717623550676">"स्प्लिट करा"</string>
<string name="action_save_app_pair" msgid="5974823919237645229">"ॲपची जोडी सेव्ह करा"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"स्प्लिट स्क्रीन वापरण्यासाठी दुसऱ्या ॲपवर टॅप करा"</string>
<string name="toast_contextual_split_select_app" msgid="433510957123687090">"स्प्लिट स्क्रीन वापरण्यासाठी दुसरे ॲप निवडा"</string>
diff --git a/quickstep/res/values-night/colors.xml b/quickstep/res/values-night/colors.xml
index 8d03ce6..94100ba 100644
--- a/quickstep/res/values-night/colors.xml
+++ b/quickstep/res/values-night/colors.xml
@@ -22,7 +22,7 @@
<color name="mock_webpage_url_bar">#202124</color>
<color name="mock_webpage_url_bar_item">#3c4043</color>
- <color name="all_set_page_background">#FF000000</color>
+ <color name="all_set_page_background">@android:color/system_neutral1_900</color>
<!-- Turn on work apps button -->
<color name="work_turn_on_stroke">?androidprv:attr/colorAccentSecondaryVariant</color>
diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml
index dd00469..47498cc 100644
--- a/quickstep/res/values-pt-rPT/strings.xml
+++ b/quickstep/res/values-pt-rPT/strings.xml
@@ -57,7 +57,7 @@
<string name="back_gesture_intro_title" msgid="19551256430224428">"Deslize rapidamente com o dedo para retroceder"</string>
<string name="back_gesture_intro_subtitle" msgid="7912576483031802797">"Para voltar ao último ecrã, deslize rapidamente do limite esquerdo ou direito até ao centro do ecrã."</string>
<string name="back_gesture_spoken_intro_subtitle" msgid="2162043199263088592">"Para voltar ao último ecrã, deslize rapidamente com 2 dedos a partir da extremidade esquerda ou direita até ao centro do ecrã."</string>
- <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"Voltar"</string>
+ <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"Retroceder"</string>
<string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Deslize rapidamente a partir da extremidade esquerda ou direita para o meio do ecrã"</string>
<string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Deslize rapidamente com o dedo a partir do limite inferior do ecrã"</string>
<string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Não faça uma pausa antes de soltar"</string>
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 14a916f..0f997f9 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -76,7 +76,7 @@
<color name="mock_webpage_top_bar_item">#80868b</color>
<color name="mock_webpage_page_text">#bdc1c6</color>
- <color name="all_set_page_background">#FFFFFFFF</color>
+ <color name="all_set_page_background">@android:color/system_neutral1_50</color>
<!-- Recents overview -->
<color name="recents_filter_icon">#333333</color>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 5c82c99..d4f66e2 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -445,6 +445,7 @@
<dimen name="bubblebar_elevation">1dp</dimen>
<dimen name="bubblebar_drag_elevation">2dp</dimen>
<dimen name="bubblebar_hotseat_adjustment_threshold">90dp</dimen>
+ <dimen name="bubblebar_bounce_distance">20dp</dimen>
<dimen name="bubblebar_icon_size_small">32dp</dimen>
<dimen name="bubblebar_icon_size">36dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 943c08c..7e52ea1 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -43,17 +43,18 @@
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.popup.PopupDataProvider;
-import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.widget.BaseWidgetSheet;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -240,14 +241,16 @@
);
bindWidgets(allWidgets);
if (mUiSurface != null) {
- Map<PackageUserKey, List<WidgetItem>> allWidgetsMap = allWidgets.stream()
- .filter(WidgetsListHeaderEntry.class::isInstance)
+ Map<ComponentKey, WidgetItem> allWidgetItems = allWidgets.stream()
+ .filter(entry -> entry instanceof WidgetsListContentEntry)
+ .flatMap(entry -> entry.mWidgets.stream())
+ .distinct()
.collect(Collectors.toMap(
- entry -> PackageUserKey.fromPackageItemInfo(entry.mPkgItem),
- entry -> entry.mWidgets)
- );
+ widget -> new ComponentKey(widget.componentName, widget.user),
+ Function.identity()
+ ));
mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(),
- mUiSurface, allWidgetsMap);
+ mUiSurface, allWidgetItems);
mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets);
}
});
diff --git a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
index 8431396..5730273 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
@@ -40,7 +40,6 @@
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.picker.WidgetRecommendationCategoryProvider;
@@ -67,10 +66,10 @@
@NonNull
private final String mUiSurface;
@NonNull
- private final Map<PackageUserKey, List<WidgetItem>> mAllWidgets;
+ private final Map<ComponentKey, WidgetItem> mAllWidgets;
public WidgetPredictionsRequester(Context context, @NonNull String uiSurface,
- @NonNull Map<PackageUserKey, List<WidgetItem>> allWidgets) {
+ @NonNull Map<ComponentKey, WidgetItem> allWidgets) {
mContext = context;
mUiSurface = uiSurface;
mAllWidgets = Collections.unmodifiableMap(allWidgets);
@@ -172,33 +171,19 @@
*/
@VisibleForTesting
static List<WidgetItem> filterPredictions(List<AppTarget> predictions,
- Map<PackageUserKey, List<WidgetItem>> allWidgets, Predicate<WidgetItem> filter) {
+ Map<ComponentKey, WidgetItem> allWidgets, Predicate<WidgetItem> filter) {
List<WidgetItem> servicePredictedItems = new ArrayList<>();
- List<WidgetItem> localFilteredWidgets = new ArrayList<>();
for (AppTarget prediction : predictions) {
- List<WidgetItem> widgetsInPackage = allWidgets.get(
- new PackageUserKey(prediction.getPackageName(), prediction.getUser()));
- if (widgetsInPackage == null || widgetsInPackage.isEmpty()) {
- continue;
- }
String className = prediction.getClassName();
if (!TextUtils.isEmpty(className)) {
- WidgetItem item = widgetsInPackage.stream()
- .filter(w -> className.equals(w.componentName.getClassName()))
- .filter(filter)
- .findFirst().orElse(null);
- if (item != null) {
- servicePredictedItems.add(item);
- continue;
+ WidgetItem widgetItem = allWidgets.get(
+ new ComponentKey(new ComponentName(prediction.getPackageName(), className),
+ prediction.getUser()));
+ if (widgetItem != null && filter.test(widgetItem)) {
+ servicePredictedItems.add(widgetItem);
}
}
- // No widget was added by the service, try local filtering
- widgetsInPackage.stream().filter(filter).findFirst()
- .ifPresent(localFilteredWidgets::add);
- }
- if (servicePredictedItems.isEmpty()) {
- servicePredictedItems.addAll(localFilteredWidgets);
}
return servicePredictedItems;
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
index 747612d..4c24d95 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -19,6 +19,7 @@
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
import android.animation.Animator;
@@ -74,8 +75,9 @@
mOnAttachListener = new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
- CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(),
- mCrossWindowBlurListener);
+ UI_HELPER_EXECUTOR.execute(() ->
+ CrossWindowBlurListeners.getInstance().addListener(
+ mLauncher.getMainExecutor(), mCrossWindowBlurListener));
mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener);
// To handle the case where window token is invalid during last setDepth call.
@@ -108,7 +110,9 @@
private void removeSecondaryListeners() {
if (mCrossWindowBlurListener != null) {
- CrossWindowBlurListeners.getInstance().removeListener(mCrossWindowBlurListener);
+ UI_HELPER_EXECUTOR.execute(() ->
+ CrossWindowBlurListeners.getInstance()
+ .removeListener(mCrossWindowBlurListener));
}
if (mOpaquenessListener != null) {
mLauncher.getScrimView().removeOpaquenessListener(mOpaquenessListener);
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index d6ee92f..73819b3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -15,12 +15,7 @@
*/
package com.android.launcher3.taskbar;
-import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED;
-
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
import android.animation.Animator;
-import android.app.ActivityOptions;
import android.view.KeyEvent;
import android.view.animation.AnimationUtils;
import android.window.RemoteTransition;
@@ -31,13 +26,10 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.SlideInRemoteTransition;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import java.io.PrintWriter;
@@ -158,28 +150,8 @@
AnimationUtils.loadInterpolator(
context, android.R.interpolator.fast_out_extra_slow_in)),
"SlideInTransition");
- if (task instanceof DesktopTask) {
- UI_HELPER_EXECUTOR.execute(() ->
- SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
- .showDesktopApps(
- mKeyboardQuickSwitchView.getDisplay().getDisplayId(),
- remoteTransition));
- } else if (mOnDesktop) {
- UI_HELPER_EXECUTOR.execute(() ->
- SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
- .showDesktopApp(task.task1.key.id));
- } else if (task.task2 == null) {
- UI_HELPER_EXECUTOR.execute(() -> {
- ActivityOptions activityOptions = mControllers.taskbarActivityContext
- .makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
- activityOptions.setRemoteTransition(remoteTransition);
-
- ActivityManagerWrapper.getInstance().startActivityFromRecents(
- task.task1.key, activityOptions);
- });
- } else {
- mControllers.uiController.launchSplitTasks(task, remoteTransition);
- }
+ mControllers.taskbarActivityContext.handleGroupTaskLaunch(
+ task, remoteTransition, mOnDesktop);
return -1;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 6b62c86..5020206 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -67,6 +67,7 @@
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.Toast;
+import android.window.RemoteTransition;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -131,6 +132,9 @@
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.NavHandle;
import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.DesktopTask;
+import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
@@ -298,7 +302,7 @@
TaskbarEduTooltipController.newInstance(this),
new KeyboardQuickSwitchController(),
new TaskbarPinningController(this, () ->
- DisplayController.INSTANCE.get(this).getInfo().isInDesktopMode()),
+ DisplayController.isInDesktopMode(this)),
bubbleControllersOptional);
mLauncherPrefs = LauncherPrefs.get(this);
@@ -1081,10 +1085,9 @@
RecentsView recents = taskbarUIController.getRecentsView();
boolean shouldCloseAllOpenViews = true;
Object tag = view.getTag();
- if (tag instanceof Task) {
- Task task = (Task) tag;
- ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
- ActivityOptions.makeBasic());
+ if (tag instanceof GroupTask groupTask) {
+ handleGroupTaskLaunch(groupTask, /* remoteTransition = */ null,
+ DisplayController.isInDesktopMode(this));
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
} else if (tag instanceof FolderInfo) {
// Tapping an expandable folder icon on Taskbar
@@ -1185,6 +1188,36 @@
}
/**
+ * Launches the given GroupTask with the following behavior:
+ * - If the GroupTask is a DesktopTask, launch the tasks in that Desktop.
+ * - If {@code onDesktop}, bring the given GroupTask to the front.
+ * - If the GroupTask is a single task, launch it via startActivityFromRecents.
+ * - Otherwise, we assume the GroupTask is a Split pair and launch them together.
+ */
+ public void handleGroupTaskLaunch(GroupTask task, @Nullable RemoteTransition remoteTransition,
+ boolean onDesktop) {
+ if (task instanceof DesktopTask) {
+ UI_HELPER_EXECUTOR.execute(() ->
+ SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplay().getDisplayId(),
+ remoteTransition));
+ } else if (onDesktop) {
+ UI_HELPER_EXECUTOR.execute(() ->
+ SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id));
+ } else if (task.task2 == null) {
+ UI_HELPER_EXECUTOR.execute(() -> {
+ ActivityOptions activityOptions =
+ makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
+ activityOptions.setRemoteTransition(remoteTransition);
+
+ ActivityManagerWrapper.getInstance().startActivityFromRecents(
+ task.task1.key, activityOptions);
+ });
+ } else {
+ mControllers.uiController.launchSplitTasks(task, remoteTransition);
+ }
+ }
+
+ /**
* Runs when the user taps a Taskbar icon in TaskbarActivityContext (Overview or inside an app),
* and calls the appropriate method to animate and launch.
*/
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 4f5922c..efe42fb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -82,6 +82,7 @@
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.views.BubbleTextHolder;
import com.android.quickstep.LauncherActivityInterface;
+import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LogUtils;
import com.android.quickstep.util.MultiValueUpdateListener;
import com.android.systemui.shared.recents.model.Task;
@@ -181,7 +182,9 @@
private DragView startInternalDrag(
BubbleTextView btv, @Nullable DragPreviewProvider dragPreviewProvider) {
- float iconScale = btv.getIcon().getAnimatedScale();
+ // TODO(b/344038728): null check is only necessary because Recents doesn't use
+ // FastBitmapDrawable
+ float iconScale = btv.getIcon() == null ? 1f : btv.getIcon().getAnimatedScale();
// Clear the pressed state if necessary
btv.clearFocus();
@@ -248,7 +251,7 @@
dragLayerX + dragOffset.x,
dragLayerY + dragOffset.y,
(View target, DropTarget.DragObject d, boolean success) -> {} /* DragSource */,
- (ItemInfo) btv.getTag(),
+ btv.getTag() instanceof ItemInfo itemInfo ? itemInfo : null,
dragRect,
scale * iconScale,
scale,
@@ -288,7 +291,9 @@
initialDragViewScale,
dragViewScaleOnDrop,
scalePx);
- dragView.setItemInfo(dragInfo);
+ if (dragInfo != null) {
+ dragView.setItemInfo(dragInfo);
+ }
mDragObject.dragComplete = false;
mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
@@ -301,7 +306,8 @@
mDragObject.dragSource = source;
mDragObject.dragInfo = dragInfo;
- mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
+ mDragObject.originalDragInfo =
+ mDragObject.dragInfo != null ? mDragObject.dragInfo.makeShallowCopy() : null;
if (mOptions.preDragCondition != null) {
dragView.setHasDragOffset(mOptions.preDragCondition.getDragOffset().x != 0
@@ -431,8 +437,8 @@
null, item.user));
}
intent.putExtra(Intent.EXTRA_USER, item.user);
- } else if (tag instanceof Task) {
- Task task = (Task) tag;
+ } else if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
+ Task task = groupTask.task1;
clipDescription = new ClipDescription(task.titleDescription,
new String[] {
ClipDescription.MIMETYPE_APPLICATION_TASK
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 2b0e169..0b7ae39 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -15,9 +15,6 @@
*/
package com.android.launcher3.taskbar;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
-import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
-
import android.util.SparseArray;
import android.view.View;
@@ -29,7 +26,6 @@
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
@@ -37,8 +33,7 @@
import com.android.launcher3.util.LauncherBindableItemsContainer;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.LauncherActivityInterface;
-import com.android.quickstep.RecentsModel;
+import com.android.quickstep.util.GroupTask;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -54,7 +49,7 @@
* Launcher model Callbacks for rendering taskbar.
*/
public class TaskbarModelCallbacks implements
- BgDataModel.Callbacks, LauncherBindableItemsContainer, RecentsModel.RunningTasksListener {
+ BgDataModel.Callbacks, LauncherBindableItemsContainer {
private final SparseArray<ItemInfo> mHotseatItems = new SparseArray<>();
private List<ItemInfo> mPredictedItems = Collections.emptyList();
@@ -68,8 +63,6 @@
// Used to defer any UI updates during the SUW unstash animation.
private boolean mDeferUpdatesForSUW;
private Runnable mDeferredUpdates;
- private final DesktopVisibilityController.DesktopVisibilityListener mDesktopVisibilityListener =
- visible -> updateRunningApps();
public TaskbarModelCallbacks(
TaskbarActivityContext context, TaskbarView container) {
@@ -79,39 +72,6 @@
public void init(TaskbarControllers controllers) {
mControllers = controllers;
- if (mControllers.taskbarRecentAppsController.getCanShowRunningApps()) {
- RecentsModel.INSTANCE.get(mContext).registerRunningTasksListener(this);
-
- if (shouldShowRunningAppsInDesktopMode()) {
- DesktopVisibilityController desktopVisibilityController =
- LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
- if (desktopVisibilityController != null) {
- desktopVisibilityController.registerDesktopVisibilityListener(
- mDesktopVisibilityListener);
- }
- }
- }
- }
-
- /**
- * Unregisters listeners in this class.
- */
- public void unregisterListeners() {
- RecentsModel.INSTANCE.get(mContext).unregisterRunningTasksListener();
-
- if (shouldShowRunningAppsInDesktopMode()) {
- DesktopVisibilityController desktopVisibilityController =
- LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
- if (desktopVisibilityController != null) {
- desktopVisibilityController.unregisterDesktopVisibilityListener(
- mDesktopVisibilityListener);
- }
- }
- }
-
- private boolean shouldShowRunningAppsInDesktopMode() {
- // TODO(b/335401172): unify DesktopMode checks in Launcher
- return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps();
}
@Override
@@ -171,7 +131,7 @@
final int itemCount = mContainer.getChildCount();
for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
View item = mContainer.getChildAt(itemIdx);
- if (op.evaluate((ItemInfo) item.getTag(), item)) {
+ if (item.getTag() instanceof ItemInfo itemInfo && op.evaluate(itemInfo, item)) {
return;
}
}
@@ -232,26 +192,30 @@
predictionNextIndex++;
}
}
- hotseatItemInfos = mControllers.taskbarRecentAppsController
- .updateHotseatItemInfos(hotseatItemInfos);
- Set<String> runningPackages = mControllers.taskbarRecentAppsController.getRunningApps();
- Set<String> minimizedPackages = mControllers.taskbarRecentAppsController.getMinimizedApps();
+
+ final TaskbarRecentAppsController recentAppsController =
+ mControllers.taskbarRecentAppsController;
+ hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos);
+ Set<String> runningPackages = recentAppsController.getRunningAppPackages();
+ Set<String> minimizedPackages = recentAppsController.getMinimizedAppPackages();
if (mDeferUpdatesForSUW) {
ItemInfo[] finalHotseatItemInfos = hotseatItemInfos;
mDeferredUpdates = () ->
- commitHotseatItemUpdates(finalHotseatItemInfos, runningPackages,
+ commitHotseatItemUpdates(finalHotseatItemInfos,
+ recentAppsController.getShownTasks(), runningPackages,
minimizedPackages);
} else {
- commitHotseatItemUpdates(hotseatItemInfos, runningPackages, minimizedPackages);
+ commitHotseatItemUpdates(hotseatItemInfos,
+ recentAppsController.getShownTasks(), runningPackages, minimizedPackages);
}
}
- private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, Set<String> runningPackages,
- Set<String> minimizedPackages) {
- mContainer.updateHotseatItems(hotseatItemInfos);
- mControllers.taskbarViewController.updateIconViewsRunningStates(runningPackages,
- minimizedPackages);
+ private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks,
+ Set<String> runningPackages, Set<String> minimizedPackages) {
+ mContainer.updateHotseatItems(hotseatItemInfos, recentTasks);
+ mControllers.taskbarViewController.updateIconViewsRunningStates(
+ runningPackages, minimizedPackages);
}
/**
@@ -270,21 +234,11 @@
}
}
- @Override
- public void onRunningTasksChanged() {
- updateRunningApps();
- }
-
/** Called when there's a change in running apps to update the UI. */
public void commitRunningAppsToUI() {
commitItemsToUI();
}
- /** Call TaskbarRecentAppsController to update running apps with mHotseatItems. */
- public void updateRunningApps() {
- mControllers.taskbarRecentAppsController.updateRunningApps();
- }
-
@Override
public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) {
mControllers.taskbarPopupController.setDeepShortcutMap(deepShortcutMapCopy);
@@ -296,7 +250,6 @@
Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
Preconditions.assertUIThread();
mControllers.taskbarAllAppsController.setApps(apps, flags, packageUserKeytoUidMap);
- mControllers.taskbarRecentAppsController.setApps(apps);
}
protected void dumpLogs(String prefix, PrintWriter pw) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index 2730be1..b697590 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -148,8 +148,8 @@
icon.clearFocus();
return null;
}
- ItemInfo item = (ItemInfo) icon.getTag();
- if (!ShortcutUtil.supportsShortcuts(item)) {
+ // TODO(b/344657629) support GroupTask as well, for Taskbar Recent apps
+ if (!(icon.getTag() instanceof ItemInfo item) || !ShortcutUtil.supportsShortcuts(item)) {
return null;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index b1fc9cc..fc3b4c7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -15,19 +15,20 @@
*/
package com.android.launcher3.taskbar
-import android.app.ActivityManager.RunningTaskInfo
-import android.app.WindowConfiguration
import androidx.annotation.VisibleForTesting
import com.android.launcher3.Flags.enableRecentsInTaskbar
-import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.ItemInfo
-import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
+import com.android.launcher3.util.CancellableTask
import com.android.quickstep.RecentsModel
+import com.android.quickstep.util.DesktopTask
+import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.Task
import com.android.window.flags.Flags.enableDesktopWindowingMode
import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
import java.io.PrintWriter
+import java.util.function.Consumer
/**
* Provides recent apps functionality, when the Taskbar Recent Apps section is enabled. Behavior:
@@ -42,22 +43,28 @@
) : LoggableTaskbarController {
// TODO(b/335401172): unify DesktopMode checks in Launcher.
- val canShowRunningApps =
+ var canShowRunningApps =
enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps()
+ @VisibleForTesting
+ set(isEnabledFromTest) {
+ field = isEnabledFromTest
+ }
// TODO(b/343532825): Add a setting to disable Recents even when the flag is on.
- var isEnabled: Boolean = enableRecentsInTaskbar() || canShowRunningApps
+ var canShowRecentApps = enableRecentsInTaskbar()
@VisibleForTesting
- set(isEnabledFromTest){
+ set(isEnabledFromTest) {
field = isEnabledFromTest
}
// Initialized in init.
private lateinit var controllers: TaskbarControllers
- private var apps: Array<AppInfo>? = null
- private var allRunningDesktopAppInfos: List<AppInfo>? = null
- private var allMinimizedDesktopAppInfos: List<AppInfo>? = null
+ private var shownHotseatItems: List<ItemInfo> = emptyList()
+ private var allRecentTasks: List<GroupTask> = emptyList()
+ private var desktopTask: DesktopTask? = null
+ var shownTasks: List<GroupTask> = emptyList()
+ private set
private val desktopVisibilityController: DesktopVisibilityController?
get() = desktopVisibilityControllerProvider()
@@ -65,122 +72,170 @@
private val isInDesktopMode: Boolean
get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
- val runningApps: Set<String>
+ val runningAppPackages: Set<String>
+ /**
+ * Returns the package names of apps that should be indicated as "running" to the user.
+ * Specifically, we return all the open tasks if we are in Desktop mode, else emptySet().
+ */
get() {
- if (!isEnabled || !isInDesktopMode) {
+ if (!canShowRunningApps || !isInDesktopMode) {
return emptySet()
}
- return allRunningDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() ?: emptySet()
+ val tasks = desktopTask?.tasks ?: return emptySet()
+ return tasks.map { task -> task.key.packageName }.toSet()
}
- val minimizedApps: Set<String>
+ val minimizedAppPackages: Set<String>
+ /**
+ * Returns the package names of apps that should be indicated as "minimized" to the user.
+ * Specifically, we return all the running packages where all the tasks in that package are
+ * minimized (not visible).
+ */
get() {
- if (!isInDesktopMode) {
+ if (!canShowRunningApps || !isInDesktopMode) {
return emptySet()
}
- return allMinimizedDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet()
- ?: emptySet()
+ val desktopTasks = desktopTask?.tasks ?: return emptySet()
+ val packageToTasks = desktopTasks.groupBy { it.key.packageName }
+ return packageToTasks.filterValues { tasks -> tasks.all { !it.isVisible } }.keys
}
+ private val recentTasksChangedListener =
+ RecentsModel.RecentTasksChangedListener { reloadRecentTasksIfNeeded() }
+
+ private val iconLoadRequests: MutableSet<CancellableTask<*>> = HashSet()
+
+ // TODO(b/343291428): add TaskVisualsChangListener as well (for calendar/clock?)
+
+ // Used to keep track of the last requested task list ID, so that we do not request to load the
+ // tasks again if we have already requested it and the task list has not changed
+ private var taskListChangeId = -1
+
fun init(taskbarControllers: TaskbarControllers) {
controllers = taskbarControllers
+ recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener)
+ reloadRecentTasksIfNeeded()
}
fun onDestroy() {
- apps = null
- }
-
- /** Stores the current [AppInfo] instances, no-op except in desktop environment. */
- fun setApps(apps: Array<AppInfo>?) {
- this.apps = apps
+ recentsModel.unregisterRecentTasksChangedListener()
+ iconLoadRequests.forEach { it.cancel() }
+ iconLoadRequests.clear()
}
/** Called to update hotseatItems, in order to de-dupe them from Recent/Running tasks later. */
- // TODO(next CL): add new section of Tasks instead of changing Hotseat items
fun updateHotseatItemInfos(hotseatItems: Array<ItemInfo?>): Array<ItemInfo?> {
- if (!isEnabled || !isInDesktopMode) {
+ // Ignore predicted apps - we show running or recent apps instead.
+ val removePredictions =
+ (isInDesktopMode && canShowRunningApps) || (!isInDesktopMode && canShowRecentApps)
+ if (!removePredictions) {
+ shownHotseatItems = hotseatItems.filterNotNull()
+ onRecentsOrHotseatChanged()
return hotseatItems
}
- val newHotseatItemInfos =
+ shownHotseatItems =
hotseatItems
.filterNotNull()
- // Ignore predicted apps - we show running apps instead
.filter { itemInfo -> !itemInfo.isPredictedItem }
.toMutableList()
- val runningDesktopAppInfos =
- allRunningDesktopAppInfos?.let {
- getRunningDesktopAppInfosExceptHotseatApps(it, newHotseatItemInfos.toList())
+
+ onRecentsOrHotseatChanged()
+
+ return shownHotseatItems.toTypedArray()
+ }
+
+ private fun reloadRecentTasksIfNeeded() {
+ if (!recentsModel.isTaskListValid(taskListChangeId)) {
+ taskListChangeId =
+ recentsModel.getTasks { tasks ->
+ allRecentTasks = tasks
+ desktopTask = allRecentTasks.filterIsInstance<DesktopTask>().firstOrNull()
+ onRecentsOrHotseatChanged()
+ controllers.taskbarViewController.commitRunningAppsToUI()
+ }
+ }
+ }
+
+ private fun onRecentsOrHotseatChanged() {
+ shownTasks =
+ if (isInDesktopMode) {
+ computeShownRunningTasks()
+ } else {
+ computeShownRecentTasks()
}
- if (runningDesktopAppInfos != null) {
- newHotseatItemInfos.addAll(runningDesktopAppInfos)
+
+ for (groupTask in shownTasks) {
+ for (task in groupTask.tasks) {
+ val callback =
+ Consumer<Task> { controllers.taskbarViewController.onTaskUpdated(it) }
+ val cancellableTask = recentsModel.iconCache.updateIconInBackground(task, callback)
+ if (cancellableTask != null) {
+ iconLoadRequests.add(cancellableTask)
+ }
+ }
}
- return newHotseatItemInfos.toTypedArray()
}
- private fun getRunningDesktopAppInfosExceptHotseatApps(
- allRunningDesktopAppInfos: List<AppInfo>,
- hotseatItems: List<ItemInfo>
- ): List<ItemInfo> {
- val hotseatPackages = hotseatItems.map { it.targetPackage }
- return allRunningDesktopAppInfos
- .filter { appInfo -> !hotseatPackages.contains(appInfo.targetPackage) }
- .map { WorkspaceItemInfo(it) }
- }
-
- private fun getDesktopRunningTasks(): List<RunningTaskInfo> =
- recentsModel.runningTasks.filter { taskInfo: RunningTaskInfo ->
- taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM
- }
-
- // TODO(b/335398876) fetch app icons from Tasks instead of AppInfos
- private fun getAppInfosFromRunningTasks(tasks: List<RunningTaskInfo>): List<AppInfo> {
- // Early return if apps is empty, since we then have no AppInfo to compare to
- if (apps == null) {
+ private fun computeShownRunningTasks(): List<GroupTask> {
+ if (!canShowRunningApps) {
return emptyList()
}
- val packageNames = tasks.map { it.realActivity?.packageName }.distinct().filterNotNull()
- return packageNames
- .map { packageName -> apps?.find { app -> packageName == app.targetPackage } }
- .filterNotNull()
+ val tasks = desktopTask?.tasks ?: emptyList()
+ // Kind of hacky, we wrap each single task in the Desktop as a GroupTask.
+ var desktopTaskAsList = tasks.map { GroupTask(it) }
+ // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too.
+ desktopTaskAsList = dedupeHotseatTasks(desktopTaskAsList, shownHotseatItems)
+ val desktopPackages = desktopTaskAsList.map { it.packageNames }
+ // Remove any missing Tasks.
+ val newShownTasks = shownTasks.filter { it.packageNames in desktopPackages }.toMutableList()
+ val newShownPackages = newShownTasks.map { it.packageNames }
+ // Add any new Tasks, maintaining the order from previous shownTasks.
+ newShownTasks.addAll(desktopTaskAsList.filter { it.packageNames !in newShownPackages })
+ return newShownTasks.toList()
}
- /** Called to update the list of currently running apps, no-op except in desktop environment. */
- fun updateRunningApps() {
- if (!isEnabled || !isInDesktopMode) {
- return controllers.taskbarViewController.commitRunningAppsToUI()
+ private fun computeShownRecentTasks(): List<GroupTask> {
+ if (!canShowRecentApps || allRecentTasks.isEmpty()) {
+ return emptyList()
}
- val runningTasks = getDesktopRunningTasks()
- val runningAppInfo = getAppInfosFromRunningTasks(runningTasks)
- allRunningDesktopAppInfos = runningAppInfo
- updateMinimizedApps(runningTasks, runningAppInfo)
- controllers.taskbarViewController.commitRunningAppsToUI()
+ // Remove the current task.
+ val allRecentTasks = allRecentTasks.subList(0, allRecentTasks.size - 1)
+ // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too
+ var shownTasks = dedupeHotseatTasks(allRecentTasks, shownHotseatItems)
+ if (shownTasks.size > MAX_RECENT_TASKS) {
+ // Remove any tasks older than MAX_RECENT_TASKS.
+ shownTasks = shownTasks.subList(shownTasks.size - MAX_RECENT_TASKS, shownTasks.size)
+ }
+ return shownTasks
}
- private fun updateMinimizedApps(
- runningTasks: List<RunningTaskInfo>,
- runningAppInfo: List<AppInfo>,
- ) {
- val allRunningAppTasks =
- runningAppInfo
- .mapNotNull { appInfo -> appInfo.targetPackage?.let { appInfo to it } }
- .associate { (appInfo, targetPackage) ->
- appInfo to
- runningTasks
- .filter { it.realActivity?.packageName == targetPackage }
- .map { it.taskId }
- }
- val minimizedTaskIds = runningTasks.associate { it.taskId to !it.isVisible }
- allMinimizedDesktopAppInfos =
- allRunningAppTasks
- .filterValues { taskIds -> taskIds.all { minimizedTaskIds[it] ?: false } }
- .keys
- .toList()
+ private fun dedupeHotseatTasks(
+ groupTasks: List<GroupTask>,
+ shownHotseatItems: List<ItemInfo>
+ ): List<GroupTask> {
+ val hotseatPackages = shownHotseatItems.map { item -> item.targetPackage }
+ return groupTasks.filter { groupTask ->
+ groupTask.hasMultipleTasks() ||
+ !hotseatPackages.contains(groupTask.task1.key.packageName)
+ }
}
override fun dumpLogs(prefix: String, pw: PrintWriter) {
pw.println("$prefix TaskbarRecentAppsController:")
- pw.println("$prefix\tisEnabled=$isEnabled")
pw.println("$prefix\tcanShowRunningApps=$canShowRunningApps")
- // TODO(next CL): add more logs
+ pw.println("$prefix\tcanShowRecentApps=$canShowRecentApps")
+ pw.println("$prefix\tshownHotseatItems=${shownHotseatItems.map{item->item.targetPackage}}")
+ pw.println("$prefix\tallRecentTasks=${allRecentTasks.map { it.packageNames }}")
+ pw.println("$prefix\tdesktopTask=${desktopTask?.packageNames}")
+ pw.println("$prefix\tshownTasks=${shownTasks.map { it.packageNames }}")
+ pw.println("$prefix\trunningTasks=$runningAppPackages")
+ pw.println("$prefix\tminimizedTasks=$minimizedAppPackages")
+ }
+
+ private val GroupTask.packageNames: List<String>
+ get() = tasks.map { task -> task.key.packageName }
+
+ private companion object {
+ const val MAX_RECENT_TASKS = 2
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 7ff887c..6279903 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -245,7 +245,7 @@
private Animator mTaskbarBackgroundAlphaAnimator;
private long mTaskbarBackgroundDuration;
- private boolean mIsGoingHome;
+ private boolean mUserIsNotGoingHome = false;
// Evaluate whether the handle should be stashed
private final LongPredicate mIsStashedPredicate = flags -> {
@@ -828,17 +828,13 @@
private boolean mTaskbarBgAlphaAnimationStarted = false;
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
- if (mIsGoingHome) {
- mTaskbarBgAlphaAnimationStarted = true;
- }
if (mTaskbarBgAlphaAnimationStarted) {
return;
}
if (valueAnimator.getAnimatedFraction() >= ANIMATED_FRACTION_THRESHOLD) {
- if (!mIsGoingHome) {
+ if (mUserIsNotGoingHome) {
playTaskbarBackgroundAlphaAnimation();
- setUserIsGoingHome(false);
mTaskbarBgAlphaAnimationStarted = true;
}
}
@@ -850,8 +846,8 @@
/**
* Sets whether the user is going home based on the current gesture.
*/
- public void setUserIsGoingHome(boolean isGoingHome) {
- mIsGoingHome = isGoingHome;
+ public void setUserIsNotGoingHome(boolean userIsNotGoingHome) {
+ mUserIsNotGoingHome = userIsNotGoingHome;
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 593285f..ce281c3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -415,7 +415,7 @@
/**
* Sets whether the user is going home based on the current gesture.
*/
- public void setUserIsGoingHome(boolean isGoingHome) {
- mControllers.taskbarStashController.setUserIsGoingHome(isGoingHome);
+ public void setUserIsNotGoingHome(boolean isNotGoingHome) {
+ mControllers.taskbarStashController.setUserIsNotGoingHome(isNotGoingHome);
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 570221c..c42d6c6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR;
import static com.android.launcher3.Flags.enableCursorHoverStates;
+import static com.android.launcher3.Flags.enableRecentsInTaskbar;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR;
@@ -30,6 +31,7 @@
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.DisplayCutout;
@@ -67,7 +69,11 @@
import com.android.launcher3.views.IconButtonView;
import com.android.quickstep.DeviceConfigWrapper;
import com.android.quickstep.util.AssistStateManager;
+import com.android.quickstep.util.DesktopTask;
+import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.recents.model.Task;
+import java.util.List;
import java.util.function.Predicate;
/**
@@ -168,7 +174,7 @@
mAllAppsButton.setForegroundTint(
mActivityContext.getColor(R.color.all_apps_button_color));
- if (enableTaskbarPinning()) {
+ if (enableTaskbarPinning() || enableRecentsInTaskbar()) {
mTaskbarDivider = (IconButtonView) LayoutInflater.from(context).inflate(
R.layout.taskbar_divider,
this, false);
@@ -308,9 +314,10 @@
/**
* Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
*/
- protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+ protected void updateHotseatItems(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
int nextViewIndex = 0;
int numViewsAnimated = 0;
+ boolean addedDividerForRecents = false;
if (mAllAppsButton != null) {
removeView(mAllAppsButton);
@@ -321,8 +328,8 @@
}
removeView(mQsb);
- for (int i = 0; i < hotseatItemInfos.length; i++) {
- ItemInfo hotseatItemInfo = hotseatItemInfos[i];
+ // Add Hotseat icons.
+ for (ItemInfo hotseatItemInfo : hotseatItemInfos) {
if (hotseatItemInfo == null) {
continue;
}
@@ -388,11 +395,8 @@
}
// Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
- if (hotseatView instanceof BubbleTextView
- && hotseatItemInfo instanceof WorkspaceItemInfo) {
- BubbleTextView btv = (BubbleTextView) hotseatView;
- WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) hotseatItemInfo;
-
+ if (hotseatView instanceof BubbleTextView btv
+ && hotseatItemInfo instanceof WorkspaceItemInfo workspaceInfo) {
boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo);
btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated);
if (animate) {
@@ -405,6 +409,67 @@
}
nextViewIndex++;
}
+
+ if (mTaskbarDivider != null && !recentTasks.isEmpty()) {
+ addView(mTaskbarDivider, nextViewIndex++);
+ addedDividerForRecents = true;
+ }
+
+ // Add Recent/Running icons.
+ for (GroupTask task : recentTasks) {
+ // Replace any Recent views with the appropriate type if it's not already that type.
+ final int expectedLayoutResId;
+ boolean isCollection = false;
+ if (task.hasMultipleTasks()) {
+ if (task instanceof DesktopTask) {
+ // TODO(b/316004172): use Desktop tile layout.
+ expectedLayoutResId = -1;
+ } else {
+ // TODO(b/343289567): use R.layout.app_pair_icon
+ expectedLayoutResId = -1;
+ }
+ isCollection = true;
+ } else {
+ expectedLayoutResId = R.layout.taskbar_app_icon;
+ }
+
+ View recentIcon = null;
+ while (nextViewIndex < getChildCount()) {
+ recentIcon = getChildAt(nextViewIndex);
+
+ // see if the view can be reused
+ if ((recentIcon.getSourceLayoutResId() != expectedLayoutResId)
+ || (isCollection && (recentIcon.getTag() != task))) {
+ removeAndRecycle(recentIcon);
+ recentIcon = null;
+ } else {
+ // View found
+ break;
+ }
+ }
+
+ if (recentIcon == null) {
+ if (isCollection) {
+ // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+ continue;
+ }
+
+ recentIcon = inflate(expectedLayoutResId);
+ LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
+ recentIcon.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
+ addView(recentIcon, nextViewIndex, lp);
+ }
+
+ if (recentIcon instanceof BubbleTextView btv) {
+ applyGroupTaskToBubbleTextView(btv, task);
+ }
+ setClickAndLongClickListenersForIcon(recentIcon);
+ if (enableCursorHoverStates()) {
+ setHoverListenerForIcon(recentIcon);
+ }
+ nextViewIndex++;
+ }
+
// Remove remaining views
while (nextViewIndex < getChildCount()) {
removeAndRecycle(getChildAt(nextViewIndex));
@@ -413,8 +478,8 @@
if (mAllAppsButton != null) {
addView(mAllAppsButton, mIsRtl ? getChildCount() : 0);
- // if only all apps button present, don't include divider view.
- if (mTaskbarDivider != null && getChildCount() > 1) {
+ // If there are no recent tasks, add divider after All Apps (unless it's the only view).
+ if (!addedDividerForRecents && mTaskbarDivider != null && getChildCount() > 1) {
addView(mTaskbarDivider, mIsRtl ? (getChildCount() - 1) : 1);
}
}
@@ -425,6 +490,20 @@
}
}
+ /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */
+ public void applyGroupTaskToBubbleTextView(BubbleTextView btv, GroupTask groupTask) {
+ // TODO(b/343289567): support app pairs.
+ Task task1 = groupTask.task1;
+ // TODO(b/344038728): use FastBitmapDrawable instead of Drawable, to get disabled state
+ // while dragging.
+ Drawable taskIcon = groupTask.task1.icon;
+ if (taskIcon != null) {
+ taskIcon = taskIcon.getConstantState().newDrawable().mutate();
+ }
+ btv.applyIconAndLabel(taskIcon, task1.titleDescription);
+ btv.setTag(groupTask);
+ }
+
/**
* Sets OnClickListener and OnLongClickListener for the given view.
*/
@@ -677,7 +756,8 @@
// map over all the shortcuts on the taskbar
for (int i = 0; i < getChildCount(); i++) {
View item = getChildAt(i);
- if (op.evaluate((ItemInfo) item.getTag(), item)) {
+ // TODO(b/344657629): Support GroupTask as well for notification dots/popup
+ if (item.getTag() instanceof ItemInfo itemInfo && op.evaluate(itemInfo, item)) {
return;
}
}
@@ -694,6 +774,7 @@
View item = getChildAt(i);
if (!(item.getTag() instanceof ItemInfo)) {
// Should only happen for All Apps button.
+ // Will also happen for Recent/Running app icons. (Which have GroupTask as tags)
continue;
}
ItemInfo info = (ItemInfo) item.getTag();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 55745b5..e59a016 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -70,6 +70,8 @@
import com.android.launcher3.util.MultiTranslateDelegate;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.views.IconButtonView;
+import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.recents.model.Task;
import java.io.PrintWriter;
import java.util.Set;
@@ -224,7 +226,6 @@
}
LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks);
mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
- mModelCallbacks.unregisterListeners();
}
public boolean areIconsVisible() {
@@ -520,21 +521,31 @@
for (View iconView : getIconViews()) {
if (iconView instanceof BubbleTextView btv) {
btv.updateRunningState(
- getRunningAppState(btv.getTargetPackageName(), runningPackages,
- minimizedPackages));
+ getRunningAppState(btv, runningPackages, minimizedPackages));
}
}
}
private BubbleTextView.RunningAppState getRunningAppState(
- String packageName,
+ BubbleTextView btv,
Set<String> runningPackages,
Set<String> minimizedPackages) {
- if (minimizedPackages.contains(packageName)) {
- return BubbleTextView.RunningAppState.MINIMIZED;
+ Object tag = btv.getTag();
+ if (tag instanceof ItemInfo itemInfo) {
+ if (minimizedPackages.contains(itemInfo.getTargetPackage())) {
+ return BubbleTextView.RunningAppState.MINIMIZED;
+ }
+ if (runningPackages.contains(itemInfo.getTargetPackage())) {
+ return BubbleTextView.RunningAppState.RUNNING;
+ }
}
- if (runningPackages.contains(packageName)) {
- return BubbleTextView.RunningAppState.RUNNING;
+ if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
+ if (minimizedPackages.contains(groupTask.task1.key.getPackageName())) {
+ return BubbleTextView.RunningAppState.MINIMIZED;
+ }
+ if (runningPackages.contains(groupTask.task1.key.getPackageName())) {
+ return BubbleTextView.RunningAppState.RUNNING;
+ }
}
return BubbleTextView.RunningAppState.NOT_RUNNING;
}
@@ -869,6 +880,27 @@
return mTaskbarView.isEventOverAnyItem(ev);
}
+ /** Called when there's a change in running apps to update the UI. */
+ public void commitRunningAppsToUI() {
+ mModelCallbacks.commitRunningAppsToUI();
+ }
+
+ /**
+ * To be called when the given Task is updated, so that we can tell TaskbarView to also update.
+ * @param task The Task whose e.g. icon changed.
+ */
+ public void onTaskUpdated(Task task) {
+ // Find the icon view(s) that changed.
+ for (View view : mTaskbarView.getIconViews()) {
+ if (view instanceof BubbleTextView btv
+ && view.getTag() instanceof GroupTask groupTask) {
+ if (groupTask.containsTask(task.key.id)) {
+ mTaskbarView.applyGroupTaskToBubbleTextView(btv, groupTask);
+ }
+ }
+ }
+ }
+
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "TaskbarViewController:");
@@ -888,15 +920,4 @@
mModelCallbacks.dumpLogs(prefix + "\t", pw);
}
-
- /** Called when there's a change in running apps to update the UI. */
- public void commitRunningAppsToUI() {
- mModelCallbacks.commitRunningAppsToUI();
- }
-
- /** Call TaskbarModelCallbacks to update running apps. */
- public void updateRunningApps() {
- mModelCallbacks.updateRunningApps();
- }
-
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 028df34..15e4578 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -600,7 +600,7 @@
Bitmap bitmap = createOverflowBitmap(context);
LayoutInflater inflater = LayoutInflater.from(context);
BubbleView bubbleView = (BubbleView) inflater.inflate(
- R.layout.bubblebar_item_view, mBarView, false /* attachToRoot */);
+ R.layout.bubble_bar_overflow_button, mBarView, false /* attachToRoot */);
BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
bubbleView.setOverflow(overflow, bitmap);
return overflow;
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
index 43e21f4..39d1ed7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
@@ -34,4 +34,8 @@
) : BubbleBarItem(info.key, view)
/** Represents the overflow bubble in the bubble bar. */
-data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view)
+data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem(KEY, view) {
+ companion object {
+ const val KEY = "Overflow"
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index c7c63e8..0ea5031 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -44,6 +44,7 @@
import com.android.launcher3.R;
import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator;
import com.android.launcher3.util.DisplayController;
import com.android.wm.shell.Flags;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
@@ -101,8 +102,6 @@
// During fade in animation we shift the bubble bar 1/60th of the screen width
private static final float FADE_IN_ANIM_POSITION_SHIFT = 1 / 60f;
- private static final int SCALE_IN_ANIMATION_DURATION_MS = 250;
-
/**
* Custom property to set alpha value for the bar view while a bubble is being dragged.
* Skips applying alpha to the dragged bubble.
@@ -161,11 +160,12 @@
// collapsed state and 1 to the fully expanded state.
private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1);
- /** An animator used for scaling in a new bubble to the bubble bar while expanded. */
+ /** An animator used for animating individual bubbles in the bubble bar while expanded. */
@Nullable
- private ValueAnimator mNewBubbleScaleInAnimator = null;
+ private BubbleAnimator mBubbleAnimator = null;
@Nullable
private ValueAnimator mScalePaddingAnimator;
+
@Nullable
private Animator mBubbleBarLocationAnimator = null;
@@ -258,6 +258,7 @@
}
if (!Flags.animateBubbleSizeChange()) {
setIconSizeAndPadding(newIconSize, newBubbleBarPadding);
+ return;
}
if (mScalePaddingAnimator != null && mScalePaddingAnimator.isRunning()) {
mScalePaddingAnimator.cancel();
@@ -670,38 +671,37 @@
bubble.setScaleX(0f);
bubble.setScaleY(0f);
addView(bubble, 0, lp);
- createNewBubbleScaleInAnimator(bubble);
- mNewBubbleScaleInAnimator.start();
+
+ mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
+ getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl()));
+ BubbleAnimator.Listener listener = new BubbleAnimator.Listener() {
+
+ @Override
+ public void onAnimationEnd() {
+ updateWidth();
+ mBubbleAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel() {
+ bubble.setScaleX(1);
+ bubble.setScaleY(1);
+ }
+
+ @Override
+ public void onAnimationUpdate(float animatedFraction) {
+ bubble.setScaleX(animatedFraction);
+ bubble.setScaleY(animatedFraction);
+ updateBubblesLayoutProperties(mBubbleBarLocation);
+ invalidate();
+ }
+ };
+ mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener);
} else {
addView(bubble, 0, lp);
}
}
- private void createNewBubbleScaleInAnimator(View bubble) {
- mNewBubbleScaleInAnimator = ValueAnimator.ofFloat(0, 1);
- mNewBubbleScaleInAnimator.setDuration(SCALE_IN_ANIMATION_DURATION_MS);
- mNewBubbleScaleInAnimator.addUpdateListener(animation -> {
- float animatedFraction = animation.getAnimatedFraction();
- bubble.setScaleX(animatedFraction);
- bubble.setScaleY(animatedFraction);
- updateBubblesLayoutProperties(mBubbleBarLocation);
- invalidate();
- });
- mNewBubbleScaleInAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationCancel(Animator animation) {
- bubble.setScaleX(1);
- bubble.setScaleY(1);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- updateWidth();
- mNewBubbleScaleInAnimator = null;
- }
- });
- }
-
// TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
@@ -716,6 +716,50 @@
updateContentDescription();
}
+ /** Removes the given bubble from the bubble bar. */
+ public void removeBubble(View bubble) {
+ if (isExpanded()) {
+ // TODO b/347062801 - animate the bubble bar if the last bubble is removed
+ int bubbleCount = getChildCount();
+ mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
+ bubbleCount, mBubbleBarLocation.isOnLeft(isLayoutRtl()));
+ BubbleAnimator.Listener listener = new BubbleAnimator.Listener() {
+
+ @Override
+ public void onAnimationEnd() {
+ removeView(bubble);
+ mBubbleAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel() {
+ bubble.setScaleX(0);
+ bubble.setScaleY(0);
+ }
+
+ @Override
+ public void onAnimationUpdate(float animatedFraction) {
+ bubble.setScaleX(1 - animatedFraction);
+ bubble.setScaleY(1 - animatedFraction);
+ updateBubblesLayoutProperties(mBubbleBarLocation);
+ invalidate();
+ }
+ };
+ int bubbleIndex = indexOfChild(bubble);
+ BubbleView lastBubble = (BubbleView) getChildAt(bubbleCount - 1);
+ String lastBubbleKey = lastBubble.getBubble().getKey();
+ boolean removingLastBubble =
+ BubbleBarOverflow.KEY.equals(lastBubbleKey)
+ ? bubbleIndex == bubbleCount - 2
+ : bubbleIndex == bubbleCount - 1;
+ mBubbleAnimator.animateRemovedBubble(
+ indexOfChild(bubble), indexOfChild(mSelectedBubbleView), removingLastBubble,
+ listener);
+ } else {
+ removeView(bubble);
+ }
+ }
+
// TODO: (b/283309949) animate it
@Override
public void removeView(View view) {
@@ -781,9 +825,14 @@
bv.setDragTranslationX(0f);
bv.setOffsetX(0f);
- bv.setScaleX(mIconScale);
- bv.setScaleY(mIconScale);
+ if (mBubbleAnimator == null || !mBubbleAnimator.isRunning()) {
+ // if the bubble animator is running don't set scale here, it will be set by the
+ // animator
+ bv.setScaleX(mIconScale);
+ bv.setScaleY(mIconScale);
+ }
bv.setTranslationY(ty);
+
// the position of the bubble when the bar is fully expanded
final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft);
// the position of the bubble when the bar is fully collapsed
@@ -861,9 +910,8 @@
}
final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing;
float translationX;
- if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) {
- translationX = getExpandedBubbleTranslationXDuringScaleAnimation(
- bubbleIndex, bubbleCount, onLeft);
+ if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) {
+ return mBubbleAnimator.getExpandedBubbleTranslationX(bubbleIndex) + mBubbleBarPadding;
} else if (onLeft) {
translationX = mBubbleBarPadding + (bubbleCount - bubbleIndex - 1) * iconAndSpacing;
} else {
@@ -872,51 +920,6 @@
return translationX - getScaleIconShift();
}
- /**
- * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is
- * expanded <b>and</b> a new bubble is animating in.
- *
- * <p>This method assumes that the animation is running so callers are expected to verify that
- * before calling it.
- */
- private float getExpandedBubbleTranslationXDuringScaleAnimation(
- int bubbleIndex, int bubbleCount, boolean onLeft) {
- // when the new bubble scale animation is running, a new bubble is animating in while the
- // bubble bar is expanded, so we have at least 2 bubbles in the bubble bar - the expanded
- // one, and the new one animating in.
-
- if (mNewBubbleScaleInAnimator == null) {
- // callers of this method are expected to verify that the animation is running, but the
- // compiler doesn't know that.
- return 0;
- }
- final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing;
- final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction();
- // the new bubble is scaling in from the center, so we need to adjust its translation so
- // that the distance to the adjacent bubble scales at the same rate.
- final float pivotAdjustment = -(1 - newBubbleScale) * getScaledIconSize() / 2f;
-
- if (onLeft) {
- if (bubbleIndex == 0) {
- // this is the animating bubble. use scaled spacing between it and the bubble to
- // its left
- return (bubbleCount - 1) * getScaledIconSize()
- + (bubbleCount - 2) * mExpandedBarIconsSpacing
- + newBubbleScale * mExpandedBarIconsSpacing
- + pivotAdjustment;
- }
- // when the bubble bar is on the left, only the translation of the right-most bubble
- // is affected by the scale animation.
- return (bubbleCount - bubbleIndex - 1) * iconAndSpacing;
- } else if (bubbleIndex == 0) {
- // the bubble bar is on the right, and this is the animating bubble. it only needs
- // to be adjusted for the scaling pivot.
- return pivotAdjustment;
- } else {
- return iconAndSpacing * (bubbleIndex - 1 + newBubbleScale);
- }
- }
-
private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount,
boolean onLeft) {
if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
@@ -979,9 +982,11 @@
BubbleView previouslySelectedBubble = mSelectedBubbleView;
mSelectedBubbleView = view;
mBubbleBarBackground.showArrow(view != null);
- // TODO: (b/283309949) remove animation should be implemented first, so than arrow
- // animation is adjusted, skip animation for now
- updateArrowForSelected(previouslySelectedBubble != null);
+
+ // if bubbles are being animated, the arrow position will be set as part of the animation
+ if (mBubbleAnimator == null) {
+ updateArrowForSelected(previouslySelectedBubble != null);
+ }
}
/**
@@ -1036,6 +1041,9 @@
}
private float arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation) {
+ if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) {
+ return mBubbleAnimator.getArrowPosition() + mBubbleBarPadding;
+ }
final int index = indexOfChild(mSelectedBubbleView);
final float selectedBubbleTranslationX = getExpandedBubbleTranslationX(
index, getChildCount(), bubbleBarLocation.isOnLeft(isLayoutRtl()));
@@ -1101,20 +1109,14 @@
*/
public float expandedWidth() {
final int childCount = getChildCount();
- // spaces amount is less than child count by 1, or 0 if no child views
- final float totalSpace;
- final float totalIconSize;
- if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) {
- // when this animation is running, a new bubble is animating in while the bubble bar is
- // expanded, so we have at least 2 bubbles in the bubble bar.
- final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction();
- totalSpace = (childCount - 2 + newBubbleScale) * mExpandedBarIconsSpacing;
- totalIconSize = (childCount - 1 + newBubbleScale) * getScaledIconSize();
- } else {
- totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing;
- totalIconSize = childCount * getScaledIconSize();
+ final float horizontalPadding = 2 * mBubbleBarPadding;
+ if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) {
+ return mBubbleAnimator.getExpandedWidth() + horizontalPadding;
}
- return totalIconSize + totalSpace + 2 * mBubbleBarPadding;
+ // spaces amount is less than child count by 1, or 0 if no child views
+ final float totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing;
+ final float totalIconSize = childCount * getScaledIconSize();
+ return totalIconSize + totalSpace + horizontalPadding;
}
private float collapsedWidth() {
@@ -1165,7 +1167,6 @@
return mIsAnimatingNewBubble;
}
-
private boolean hasOverview() {
// Overview is always the last bubble
View lastChild = getChildAt(getChildCount() - 1);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index eec095d..da0826b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -388,7 +388,7 @@
*/
public void removeBubble(BubbleBarItem b) {
if (b != null) {
- mBarView.removeView(b.getView());
+ mBarView.removeBubble(b.getView());
} else {
Log.w(TAG, "removeBubble, bubble was null!");
}
@@ -435,6 +435,11 @@
return;
}
+ if (mBubbleStashController.isBubblesShowingOnHome() && !isExpanding && !isExpanded()) {
+ mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble);
+ return;
+ }
+
// only animate the new bubble if we're in an app and not auto expanding
if (isInApp && !isExpanding && !isExpanded()) {
mBubbleBarViewAnimator.animateBubbleInForStashed(bubble);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
new file mode 100644
index 0000000..7672743
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.animation
+
+import androidx.core.animation.Animator
+import androidx.core.animation.ValueAnimator
+
+/**
+ * Animates individual bubbles within the bubble bar while the bubble bar is expanded.
+ *
+ * This class should only be kept for the duration of the animation and a new instance should be
+ * created for each animation.
+ */
+class BubbleAnimator(
+ private val iconSize: Float,
+ private val expandedBarIconSpacing: Float,
+ private val bubbleCount: Int,
+ private val onLeft: Boolean,
+) {
+
+ companion object {
+ const val ANIMATION_DURATION_MS = 250L
+ }
+
+ private var state: State = State.Idle
+ private lateinit var animator: ValueAnimator
+
+ fun animateNewBubble(selectedBubbleIndex: Int, listener: Listener) {
+ animator = createAnimator(listener)
+ state = State.AddingBubble(selectedBubbleIndex)
+ animator.start()
+ }
+
+ fun animateRemovedBubble(
+ bubbleIndex: Int,
+ selectedBubbleIndex: Int,
+ removingLastBubble: Boolean,
+ listener: Listener
+ ) {
+ animator = createAnimator(listener)
+ state = State.RemovingBubble(bubbleIndex, selectedBubbleIndex, removingLastBubble)
+ animator.start()
+ }
+
+ private fun createAnimator(listener: Listener): ValueAnimator {
+ val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
+ animator.addUpdateListener { animation ->
+ val animatedFraction = (animation as ValueAnimator).animatedFraction
+ listener.onAnimationUpdate(animatedFraction)
+ }
+ animator.addListener(
+ object : Animator.AnimatorListener {
+
+ override fun onAnimationCancel(animation: Animator) {
+ listener.onAnimationCancel()
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ state = State.Idle
+ listener.onAnimationEnd()
+ }
+
+ override fun onAnimationRepeat(animation: Animator) {}
+
+ override fun onAnimationStart(animation: Animator) {}
+ }
+ )
+ return animator
+ }
+
+ /**
+ * The translation X of the bubble at index [bubbleIndex] according to the progress of the
+ * animation.
+ *
+ * Callers should verify that the animation is running before calling this.
+ *
+ * @see isRunning
+ */
+ fun getExpandedBubbleTranslationX(bubbleIndex: Int): Float {
+ return when (val state = state) {
+ State.Idle -> 0f
+ is State.AddingBubble ->
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = bubbleIndex,
+ scalingBubbleIndex = 0,
+ bubbleScale = animator.animatedFraction
+ )
+ is State.RemovingBubble ->
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = bubbleIndex,
+ scalingBubbleIndex = state.bubbleIndex,
+ bubbleScale = 1 - animator.animatedFraction
+ )
+ }
+ }
+
+ /**
+ * The expanded width of the bubble bar according to the progress of the animation.
+ *
+ * Callers should verify that the animation is running before calling this.
+ *
+ * @see isRunning
+ */
+ fun getExpandedWidth(): Float {
+ val bubbleScale =
+ when (state) {
+ State.Idle -> 0f
+ is State.AddingBubble -> animator.animatedFraction
+ is State.RemovingBubble -> 1 - animator.animatedFraction
+ }
+ // When this animator is running the bubble bar is expanded so it's safe to assume that we
+ // have at least 2 bubbles, but should update the logic to support optional overflow.
+ // If we're removing the last bubble, the entire bar should animate and we shouldn't get
+ // here.
+ val totalSpace = (bubbleCount - 2 + bubbleScale) * expandedBarIconSpacing
+ val totalIconSize = (bubbleCount - 1 + bubbleScale) * iconSize
+ return totalIconSize + totalSpace
+ }
+
+ /**
+ * Returns the arrow position according to the progress of the animation and, if the selected
+ * bubble is being removed, accounting to the newly selected bubble.
+ *
+ * Callers should verify that the animation is running before calling this.
+ *
+ * @see isRunning
+ */
+ fun getArrowPosition(): Float {
+ return when (val state = state) {
+ State.Idle -> 0f
+ is State.AddingBubble -> {
+ val tx =
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = state.selectedBubbleIndex,
+ scalingBubbleIndex = 0,
+ bubbleScale = animator.animatedFraction
+ )
+ tx + iconSize / 2f
+ }
+ is State.RemovingBubble -> getArrowPositionWhenRemovingBubble(state)
+ }
+ }
+
+ private fun getArrowPositionWhenRemovingBubble(state: State.RemovingBubble): Float {
+ return if (state.selectedBubbleIndex != state.bubbleIndex) {
+ // if we're not removing the selected bubble, the selected bubble doesn't change so just
+ // return the translation X of the selected bubble and add half icon
+ val tx =
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = state.selectedBubbleIndex,
+ scalingBubbleIndex = state.bubbleIndex,
+ bubbleScale = 1 - animator.animatedFraction
+ )
+ tx + iconSize / 2f
+ } else {
+ // we're removing the selected bubble so the arrow needs to point to a different bubble.
+ // if we're removing the last bubble the newly selected bubble will be the second to
+ // last. otherwise, it'll be the next bubble (closer to the overflow)
+ val iconAndSpacing = iconSize + expandedBarIconSpacing
+ if (state.removingLastBubble) {
+ if (onLeft) {
+ // the newly selected bubble is the bubble to the right. at the end of the
+ // animation all the bubbles will have shifted left, so the arrow stays at the
+ // same distance from the left edge of bar
+ (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f
+ } else {
+ // the newly selected bubble is the bubble to the left. at the end of the
+ // animation all the bubbles will have shifted right, and the arrow would
+ // eventually be closer to the left edge of the bar by iconAndSpacing
+ val initialTx = state.bubbleIndex * iconAndSpacing + iconSize / 2f
+ initialTx - animator.animatedFraction * iconAndSpacing
+ }
+ } else {
+ if (onLeft) {
+ // the newly selected bubble is to the left, and bubbles are shifting left, so
+ // move the arrow closer to the left edge of the bar by iconAndSpacing
+ val initialTx =
+ (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f
+ initialTx - animator.animatedFraction * iconAndSpacing
+ } else {
+ // the newly selected bubble is to the right, and bubbles are shifting right, so
+ // the arrow stays at the same distance from the left edge of the bar
+ state.bubbleIndex * iconAndSpacing + iconSize / 2f
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is
+ * expanded and a bubble is animating in or out.
+ *
+ * @param bubbleIndex the index of the bubble for which the translation is requested
+ * @param scalingBubbleIndex the index of the bubble that is animating
+ * @param bubbleScale the current scale of the animating bubble
+ */
+ private fun getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex: Int,
+ scalingBubbleIndex: Int,
+ bubbleScale: Float
+ ): Float {
+ val iconAndSpacing = iconSize + expandedBarIconSpacing
+ // the bubble is scaling from the center, so we need to adjust its translation so
+ // that the distance to the adjacent bubble scales at the same rate.
+ val pivotAdjustment = -(1 - bubbleScale) * iconSize / 2f
+
+ return if (onLeft) {
+ when {
+ bubbleIndex < scalingBubbleIndex ->
+ // the bar is on the left and the current bubble is to the right of the scaling
+ // bubble so account for its scale
+ (bubbleCount - bubbleIndex - 2 + bubbleScale) * iconAndSpacing
+ bubbleIndex == scalingBubbleIndex -> {
+ // the bar is on the left and this is the scaling bubble
+ val totalIconSize = (bubbleCount - bubbleIndex - 1) * iconSize
+ // don't count the spacing between the scaling bubble and the bubble on the left
+ // because we need to scale that space
+ val totalSpacing = (bubbleCount - bubbleIndex - 2) * expandedBarIconSpacing
+ val scaledSpace = bubbleScale * expandedBarIconSpacing
+ totalIconSize + totalSpacing + scaledSpace + pivotAdjustment
+ }
+ else ->
+ // the bar is on the left and the scaling bubble is on the right. the current
+ // bubble is unaffected by the scaling bubble
+ (bubbleCount - bubbleIndex - 1) * iconAndSpacing
+ }
+ } else {
+ when {
+ bubbleIndex < scalingBubbleIndex ->
+ // the bar is on the right and the scaling bubble is on the right. the current
+ // bubble is unaffected by the scaling bubble
+ iconAndSpacing * bubbleIndex
+ bubbleIndex == scalingBubbleIndex ->
+ // the bar is on the right, and this is the animating bubble. it only needs to
+ // be adjusted for the scaling pivot.
+ iconAndSpacing * bubbleIndex + pivotAdjustment
+ else ->
+ // the bar is on the right and the scaling bubble is on the left so account for
+ // its scale
+ iconAndSpacing * (bubbleIndex - 1 + bubbleScale)
+ }
+ }
+ }
+
+ val isRunning: Boolean
+ get() = state != State.Idle
+
+ /** The state of the animation. */
+ sealed interface State {
+
+ /** The animation is not running. */
+ data object Idle : State
+
+ /** A new bubble is being added to the bubble bar. */
+ data class AddingBubble(val selectedBubbleIndex: Int) : State
+
+ /** A bubble is being removed from the bubble bar. */
+ data class RemovingBubble(
+ /** The index of the bubble being removed. */
+ val bubbleIndex: Int,
+ /** The index of the selected bubble. */
+ val selectedBubbleIndex: Int,
+ /** Whether the bubble being removed is also the last bubble. */
+ val removingLastBubble: Boolean
+ ) : State
+ }
+
+ /** Callbacks for the animation. */
+ interface Listener {
+
+ /**
+ * Notifies the listener of an animation update event, where `animatedFraction` represents
+ * the progress of the animation starting from 0 and ending at 1.
+ */
+ fun onAnimationUpdate(animatedFraction: Float)
+
+ /** Notifies the listener that the animation was canceled. */
+ fun onAnimationCancel()
+
+ /** Notifies that listener that the animation ended. */
+ fun onAnimationEnd()
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index d88e272..feff9fd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -18,8 +18,12 @@
import android.view.View
import android.view.View.VISIBLE
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.ObjectAnimator
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.SpringForce
+import com.android.launcher3.R
import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
import com.android.launcher3.taskbar.bubbles.BubbleBarView
import com.android.launcher3.taskbar.bubbles.BubbleStashController
@@ -36,6 +40,8 @@
) {
private var animatingBubble: AnimatingBubble? = null
+ private val bubbleBarBounceDistanceInPx =
+ bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
private companion object {
/** The time to show the flyout. */
@@ -44,6 +50,8 @@
const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f
/** The minimum alpha value to make the bubble bar touchable. */
const val MIN_ALPHA_FOR_TOUCHABLE = 0.5f
+ /** The duration of the bounce animation. */
+ const val BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS = 250L
}
/** Wrapper around the animating bubble with its show and hide animations. */
@@ -277,7 +285,7 @@
if (animator.isRunning()) animator.cancel()
// the animation of a new bubble is divided into 2 parts. The first part shows the bubble
// and the second part hides it after a delay if we are in an app.
- val showAnimation = buildBubbleBarBounceAnimation()
+ val showAnimation = buildBubbleBarSpringInAnimation()
val hideAnimation =
if (isInApp && !isExpanding) {
buildBubbleBarToHandleAnimation()
@@ -296,7 +304,7 @@
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
- private fun buildBubbleBarBounceAnimation() = Runnable {
+ private fun buildBubbleBarSpringInAnimation() = Runnable {
// prepare the bubble bar for the animation
bubbleBarView.onAnimatingBubbleStarted()
bubbleBarView.translationY = bubbleBarView.height.toFloat()
@@ -316,6 +324,42 @@
animator.start()
}
+ fun animateBubbleBarForCollapsed(b: BubbleBarBubble) {
+ val bubbleView = b.view
+ val animator = PhysicsAnimator.getInstance(bubbleView)
+ if (animator.isRunning()) animator.cancel()
+ val showAnimation = buildBubbleBarBounceAnimation()
+ val hideAnimation = Runnable {
+ animatingBubble = null
+ bubbleStashController.showBubbleBarImmediate()
+ bubbleBarView.onAnimatingBubbleCompleted()
+ bubbleStashController.updateTaskbarTouchRegion()
+ }
+ animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+ scheduler.post(showAnimation)
+ scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+ }
+
+ /**
+ * The bubble bar animation when it is collapsed is divided into 2 chained animations. The first
+ * animation is a regular accelerate animation that moves the bubble bar upwards. When it ends
+ * the bubble bar moves back to its initial position with a spring animation.
+ */
+ private fun buildBubbleBarBounceAnimation() = Runnable {
+ bubbleBarView.onAnimatingBubbleStarted()
+ val ty = bubbleStashController.bubbleBarTranslationY
+
+ val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView)
+ springBackAnimation.setDefaultSpringConfig(springConfig)
+ springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty)
+
+ // animate the bubble bar up and start the spring back down animation when it ends.
+ ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx)
+ .withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS)
+ .withEndAction { springBackAnimation.start() }
+ .start()
+ }
+
/** Handles touching the animating bubble bar. */
fun onBubbleBarTouchedWhileAnimating() {
PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning()
@@ -344,4 +388,20 @@
private fun <T> PhysicsAnimator<T>.cancelIfRunning() {
if (isRunning()) cancel()
}
+
+ private fun ObjectAnimator.withDuration(duration: Long): ObjectAnimator {
+ setDuration(duration)
+ return this
+ }
+
+ private fun ObjectAnimator.withEndAction(endAction: () -> Unit): ObjectAnimator {
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ endAction()
+ }
+ }
+ )
+ return this
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
index 1ec075f..ac7dd06 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
@@ -19,7 +19,6 @@
import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.launcher3.taskbar.TaskbarControllers
-import com.android.launcher3.taskbar.TaskbarRecentAppsController
import com.android.launcher3.util.DisplayController
/** Evaluates all the features taskbar can have. */
@@ -34,11 +33,14 @@
val hasNavButtons = taskbarActivityContext.isThreeButtonNav
val hasRecents: Boolean
- get() = taskbarControllers.taskbarRecentAppsController.isEnabled
+ get() = taskbarControllers.taskbarRecentAppsController.shownTasks.isNotEmpty()
val hasDivider: Boolean
get() = enableTaskbarPinning() || hasRecents
val isTransient: Boolean
get() = DisplayController.isTransientTaskbar(taskbarActivityContext)
+
+ val isLandscape: Boolean
+ get() = taskbarActivityContext.deviceProfile.isLandscape
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt
index 4cd895d..67bbcce 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt
@@ -29,13 +29,15 @@
val defaultPersistentIconSize = iconSize40dp
val defaultTransientIconSize = iconSize44dp
- // defined as row, columns
val transientTaskbarIconSizeByGridSize =
mapOf(
- Pair(6, 5) to iconSize52dp,
- Pair(4, 5) to iconSize48dp,
- Pair(5, 4) to iconSize48dp,
- Pair(4, 4) to iconSize48dp,
- Pair(5, 6) to iconSize44dp,
+ TransientTaskbarIconSizeKey(6, 5, false) to iconSize52dp,
+ TransientTaskbarIconSizeKey(6, 5, true) to iconSize52dp,
+ TransientTaskbarIconSizeKey(4, 4, false) to iconSize48dp,
+ TransientTaskbarIconSizeKey(4, 4, true) to iconSize52dp,
+ TransientTaskbarIconSizeKey(4, 5, false) to iconSize48dp,
+ TransientTaskbarIconSizeKey(4, 5, true) to iconSize48dp,
+ TransientTaskbarIconSizeKey(5, 5, false) to iconSize44dp,
+ TransientTaskbarIconSizeKey(5, 5, true) to iconSize44dp,
)
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt
index 02e5947..0b7be40 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt
@@ -22,7 +22,7 @@
fun getIconSizeByGrid(row: Int, column: Int): TaskbarIconSize {
return if (taskbarFeatureEvaluator.isTransient) {
TaskbarIconSpecs.transientTaskbarIconSizeByGridSize.getOrDefault(
- Pair(row, column),
+ TransientTaskbarIconSizeKey(row, column, taskbarFeatureEvaluator.isLandscape),
TaskbarIconSpecs.defaultTransientIconSize,
)
} else {
@@ -58,3 +58,5 @@
}
data class TaskbarIconSize(val size: Int)
+
+data class TransientTaskbarIconSizeKey(val row: Int, val column: Int, val isLandscape: Boolean)
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 2168f7a..037f2f6 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -60,6 +60,8 @@
import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FAILED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FALLBACK;
import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
import static com.android.quickstep.util.SplitAnimationTimings.TABLET_HOME_TO_SPLIT;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
@@ -172,6 +174,7 @@
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskUtils;
import com.android.quickstep.TouchInteractionService.TISBinder;
+import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.AsyncClockEventDelegate;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LauncherUnfoldAnimationController;
@@ -198,8 +201,6 @@
import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver;
import com.android.systemui.unfold.updates.RotationChangeProvider;
-import kotlin.Unit;
-
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -212,6 +213,8 @@
import java.util.function.Predicate;
import java.util.stream.Stream;
+import kotlin.Unit;
+
public class QuickstepLauncher extends Launcher implements RecentsViewContainer {
private static final boolean TRACE_LAYOUTS =
SystemProperties.getBoolean("persist.debug.trace_layouts", false);
@@ -581,9 +584,19 @@
}
case QUICK_SWITCH_STATE_ORDINAL: {
RecentsView rv = getOverviewPanel();
- TaskView tasktolaunch = rv.getCurrentPageTaskView();
- if (tasktolaunch != null) {
- tasktolaunch.launchTask(success -> {
+ TaskView currentPageTask = rv.getCurrentPageTaskView();
+ TaskView fallbackTask = rv.getTaskViewAt(0);
+ if (currentPageTask != null || fallbackTask != null) {
+ TaskView taskToLaunch = currentPageTask;
+ if (currentPageTask == null) {
+ taskToLaunch = fallbackTask;
+ ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+ "Quick switch from home fallback case: The TaskView at index ")
+ .append(rv.getCurrentPage())
+ .append(" is missing."),
+ QUICK_SWITCH_FROM_HOME_FALLBACK);
+ }
+ taskToLaunch.launchTask(success -> {
if (!success) {
getStateManager().goToState(OVERVIEW);
} else {
@@ -592,6 +605,11 @@
return Unit.INSTANCE;
});
} else {
+ ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+ "Quick switch from home failed: TaskViews at indices ")
+ .append(rv.getCurrentPage())
+ .append(" and 0 are missing."),
+ QUICK_SWITCH_FROM_HOME_FAILED);
getStateManager().goToState(NORMAL);
}
break;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
index 01d5ff0..56fc4d1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
@@ -17,7 +17,6 @@
import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetHostView;
@@ -100,7 +99,7 @@
// for concurrent modification.
new ArrayList<>(h.mProviderChangedListeners).forEach(
ProviderChangedListener::notifyWidgetProvidersChanged))),
- UI_HELPER_EXECUTOR.getLooper());
+ getWidgetHolderExecutor().getLooper());
if (WIDGETS_ENABLED) {
sWidgetHost.startListening();
}
@@ -199,8 +198,10 @@
return;
}
- sWidgetHost.setAppWidgetHidden();
- setListeningFlag(false);
+ getWidgetHolderExecutor().execute(() -> {
+ sWidgetHost.setAppWidgetHidden();
+ setListeningFlag(false);
+ });
}
@Override
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 1acafab..93f72fc 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1196,7 +1196,7 @@
}
if (mContainerInterface.getTaskbarController() != null) {
// Resets this value as the gesture is now complete.
- mContainerInterface.getTaskbarController().setUserIsGoingHome(false);
+ mContainerInterface.getTaskbarController().setUserIsNotGoingHome(false);
}
ActiveGestureLog.INSTANCE.addLog(
new ActiveGestureLog.CompoundString("onSettledOnEndTarget ")
@@ -1350,7 +1350,7 @@
&& mIsTransientTaskbar
&& mContainerInterface.getTaskbarController() != null) {
mContainerInterface.getTaskbarController()
- .setUserIsGoingHome(endTarget == GestureState.GestureEndTarget.HOME);
+ .setUserIsNotGoingHome(endTarget != GestureState.GestureEndTarget.HOME);
}
float endShift = endTarget == ALL_APPS ? mDragLengthFactor
diff --git a/quickstep/src/com/android/quickstep/BinderTracker.java b/quickstep/src/com/android/quickstep/BinderTracker.java
index a876cd8..2a42861 100644
--- a/quickstep/src/com/android/quickstep/BinderTracker.java
+++ b/quickstep/src/com/android/quickstep/BinderTracker.java
@@ -26,11 +26,14 @@
import android.os.Trace;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.TraceHelper;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.LinkedList;
import java.util.Set;
import java.util.function.Consumer;
@@ -43,6 +46,9 @@
public class BinderTracker {
private static final String TAG = "BinderTracker";
+ private static final Boolean DEBUG_STACKTRACE = false;
+
+ private static final String[] sActionablePackageKeywords = {"launcher3", "systemui"};
// Common IPCs that are ok to block the main thread.
private static final Set<String> sAllowedFrameworkClasses = Set.of(
@@ -145,13 +151,32 @@
if (ipcBypass == null) {
mUnexpectedTransactionCallback.accept(new BinderCallSite(
- mMainThreadTraceStack.peekLast(), descriptor, transactionCode));
+ mMainThreadTraceStack.peekLast(), descriptor, transactionCode,
+ getActionableStacktrace()));
} else {
Log.d(TAG, "MainThread-IPC " + descriptor + " ignored due to " + ipcBypass);
}
return null;
}
+ @NonNull
+ private static String getActionableStacktrace() {
+ if (!DEBUG_STACKTRACE) {
+ return "DEBUG_STACKTRACE not turned on.";
+ }
+ final StringWriter sw = new StringWriter();
+ new Throwable().printStackTrace(new PrintWriter(sw));
+ final String stackTrace = sw.toString();
+
+ for (String actionablePackageKeyword : sActionablePackageKeywords) {
+ if (stackTrace.contains(actionablePackageKeyword)) {
+ return stackTrace;
+ }
+ }
+
+ return "Not actionable to launcher";
+ }
+
@Override
public Object onTransactStarted(IBinder binder, int transactionCode) {
// Do nothing
@@ -177,11 +202,14 @@
public final String activeTrace;
public final String descriptor;
public final int transactionCode;
+ public final String stackTrace;
- BinderCallSite(String activeTrace, String descriptor, int transactionCode) {
+ BinderCallSite(
+ String activeTrace, String descriptor, int transactionCode, String stackTrace) {
this.activeTrace = activeTrace;
this.descriptor = descriptor;
this.transactionCode = transactionCode;
+ this.stackTrace = stackTrace;
}
}
}
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index c428827..1048ea1 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -35,7 +35,6 @@
import androidx.annotation.UiThread;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Flags;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.LauncherInitListener;
@@ -213,10 +212,7 @@
if (launcher.isStarted() && (isInLiveTileMode() || launcher.hasBeenResumed())) {
return launcher;
}
- if (Flags.useActivityOverlay()
- && SystemUiProxy.INSTANCE.get(launcher).getHomeVisibilityState().isHomeVisible()) {
- return launcher;
- }
+
return null;
}
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index 3c66590..e17cdcd 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -144,8 +144,6 @@
return new FloatingViewHomeAnimationFactory(floatingIconView) {
@Nullable
private RectF mTargetRect;
- @Nullable
- private RectFSpringAnim mSiblingAnimation;
@Nullable
@Override
@@ -173,14 +171,6 @@
}
@Override
- protected void playScalingRevealAnimation() {
- if (mContainer != null) {
- new ScalingWorkspaceRevealAnim(mContainer, mSiblingAnimation,
- getWindowTargetRect()).start();
- }
- }
-
- @Override
public void setAnimation(RectFSpringAnim anim) {
super.setAnimation(anim);
mSiblingAnimation = anim;
@@ -245,6 +235,8 @@
isTargetTranslucent, fallbackBackgroundColor);
return new FloatingViewHomeAnimationFactory(floatingWidgetView) {
+ @Nullable
+ private RectF mTargetRect;
@Override
@Nullable
@@ -254,8 +246,14 @@
@Override
public RectF getWindowTargetRect() {
- super.getWindowTargetRect();
- return backgroundLocation;
+ if (enableScalingRevealHomeAnimation()) {
+ if (mTargetRect == null) {
+ mTargetRect = new RectF(backgroundLocation);
+ }
+ return mTargetRect;
+ } else {
+ return backgroundLocation;
+ }
}
@Override
@@ -266,10 +264,11 @@
@Override
public void setAnimation(RectFSpringAnim anim) {
super.setAnimation(anim);
-
- anim.addAnimatorListener(floatingWidgetView);
- floatingWidgetView.setOnTargetChangeListener(anim::onTargetPositionChanged);
- floatingWidgetView.setFastFinishRunnable(anim::end);
+ mSiblingAnimation = anim;
+ mSiblingAnimation.addAnimatorListener(floatingWidgetView);
+ floatingWidgetView.setOnTargetChangeListener(
+ mSiblingAnimation::onTargetPositionChanged);
+ floatingWidgetView.setFastFinishRunnable(mSiblingAnimation::end);
}
@Override
@@ -330,14 +329,23 @@
}
private class FloatingViewHomeAnimationFactory extends LauncherHomeAnimationFactory {
-
private final FloatingView mFloatingView;
+ @Nullable
+ protected RectFSpringAnim mSiblingAnimation;
FloatingViewHomeAnimationFactory(FloatingView floatingView) {
mFloatingView = floatingView;
}
@Override
+ protected void playScalingRevealAnimation() {
+ if (mContainer != null) {
+ new ScalingWorkspaceRevealAnim(mContainer, mSiblingAnimation,
+ getWindowTargetRect()).start();
+ }
+ }
+
+ @Override
public void onCancel() {
mFloatingView.fastFinish();
}
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 7da92bc..8f533a3 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -265,6 +265,10 @@
case TYPE_HOME:
ActiveGestureLog.INSTANCE.addLog(
"OverviewCommandHelper.executeCommand(TYPE_HOME)");
+ // Although IActivityTaskManager$Stub$Proxy.startActivity is a slow binder call,
+ // we should still call it on main thread because launcher is waiting for
+ // ActivityTaskManager to resume it. Also calling startActivity() on bg thread
+ // could potentially delay resuming launcher. See b/348668521 for more details.
mService.startActivity(mOverviewComponentObserver.getHomeIntent());
return true;
case TYPE_SHOW:
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index 3c902e6..f4e68dc 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -66,7 +66,8 @@
if (BuildConfig.IS_STUDIO_BUILD) {
BinderTracker.startTracking(call -> Log.e("BinderCall",
- call.descriptor + " called on mainthread under " + call.activeTrace));
+ call.descriptor + " called on main thread under " + call.activeTrace
+ + " stackTrace: " + call.stackTrace));
}
}
}
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index b08a46f..66091d4 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -70,7 +70,8 @@
private TaskLoadResult mResultsBg = INVALID_RESULT;
private TaskLoadResult mResultsUi = INVALID_RESULT;
- private RecentsModel.RunningTasksListener mRunningTasksListener;
+ private @Nullable RecentsModel.RunningTasksListener mRunningTasksListener;
+ private @Nullable RecentsModel.RecentTasksChangedListener mRecentTasksChangedListener;
// Tasks are stored in order of least recently launched to most recently launched.
private ArrayList<ActivityManager.RunningTaskInfo> mRunningTasks;
@@ -199,6 +200,9 @@
public void onRecentTasksChanged() {
invalidateLoadedTasks();
+ if (mRecentTasksChangedListener != null) {
+ mRecentTasksChangedListener.onRecentTasksChanged();
+ }
}
private synchronized void invalidateLoadedTasks() {
@@ -221,6 +225,21 @@
mRunningTasksListener = null;
}
+ /**
+ * Registers a listener for running tasks
+ */
+ public void registerRecentTasksChangedListener(
+ RecentsModel.RecentTasksChangedListener listener) {
+ mRecentTasksChangedListener = listener;
+ }
+
+ /**
+ * Removes the previously registered running tasks listener
+ */
+ public void unregisterRecentTasksChangedListener() {
+ mRecentTasksChangedListener = null;
+ }
+
private void initRunningTasks(ArrayList<ActivityManager.RunningTaskInfo> runningTasks) {
// Tasks are retrieved in order of most recently launched/used to least recently launched.
mRunningTasks = new ArrayList<>(runningTasks);
@@ -357,6 +376,7 @@
task.setLastSnapshotData(taskInfo);
task.positionInParent = taskInfo.positionInParent;
task.appBounds = taskInfo.configuration.windowConfiguration.getAppBounds();
+ task.isVisible = taskInfo.isVisible;
tasks.add(task);
}
return new DesktopTask(tasks);
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 6eefe4a..b796951 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -42,6 +42,7 @@
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.SafeCloseable;
import com.android.quickstep.recents.data.RecentTasksDataSource;
+import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.TaskVisualsChangeListener;
import com.android.systemui.shared.recents.model.Task;
@@ -301,6 +302,8 @@
/**
* Registers a listener for running tasks
+ * TODO(b/343292503): Should we remove RunningTasksListener entirely if it's not needed?
+ * (Note that Desktop mode gets the running tasks by checking {@link DesktopTask#tasks}
*/
public void registerRunningTasksListener(RunningTasksListener listener) {
mTaskList.registerRunningTasksListener(listener);
@@ -314,6 +317,20 @@
}
/**
+ * Registers a listener for recent tasks
+ */
+ public void registerRecentTasksChangedListener(RecentTasksChangedListener listener) {
+ mTaskList.registerRecentTasksChangedListener(listener);
+ }
+
+ /**
+ * Removes the previously registered running tasks listener
+ */
+ public void unregisterRecentTasksChangedListener() {
+ mTaskList.unregisterRecentTasksChangedListener();
+ }
+
+ /**
* Gets the set of running tasks.
*/
public ArrayList<ActivityManager.RunningTaskInfo> getRunningTasks() {
@@ -379,4 +396,14 @@
*/
void onRunningTasksChanged();
}
+
+ /**
+ * Listener for receiving recent tasks changes
+ */
+ public interface RecentTasksChangedListener {
+ /**
+ * Called when there's a change to recent tasks
+ */
+ void onRecentTasksChanged();
+ }
}
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index fb54241..ba33c62 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -17,6 +17,7 @@
import static com.android.app.animation.Interpolators.ACCELERATE_1_5;
import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
import static com.android.launcher3.PagedView.INVALID_PAGE;
import android.animation.Animator;
@@ -449,7 +450,7 @@
float alpha = mAnimationFactory.getWindowAlpha(progress);
mHomeAnim.setPlayFraction(progress);
- if (mTargetTaskView == null) {
+ if (!enableAdditionalHomeAnimations() || mTargetTaskView == null) {
mHomeToWindowPositionMap.mapRect(mWindowCurrentRect, currentRect);
mMatrix.setRectToRect(mCropRectF, mWindowCurrentRect, ScaleToFit.FILL);
mLocalTransformParams
@@ -464,10 +465,15 @@
mLocalTransformParams.applySurfaceParams(
mLocalTransformParams.createSurfaceParams(this));
- mAnimationFactory.update(
- currentRect, progress, mMatrix.mapRadius(cornerRadius), (int) (alpha * 255));
- if (mTargetTaskView == null) {
+ mAnimationFactory.update(
+ currentRect,
+ progress,
+ mMatrix.mapRadius(cornerRadius),
+ !enableAdditionalHomeAnimations() || mTargetTaskView == null
+ ? 0 : (int) (alpha * 255));
+
+ if (!enableAdditionalHomeAnimations() || mTargetTaskView == null) {
return;
}
if (mAnimationFactory.isAnimatingIntoIcon() && mAnimationFactory.isAnimationReady()) {
@@ -506,7 +512,7 @@
public void onAnimationStart(Animator animation) {
setUp();
mHomeAnim.dispatchOnStart();
- if (mTargetTaskView == null) {
+ if (!enableAdditionalHomeAnimations() || mTargetTaskView == null) {
return;
}
Rect thumbnailBounds = new Rect();
@@ -521,7 +527,7 @@
}
private void setUp() {
- if (mTargetTaskView == null) {
+ if (!enableAdditionalHomeAnimations() || mTargetTaskView == null) {
return;
}
RecentsView recentsView = mTargetTaskView.getRecentsView();
@@ -542,7 +548,7 @@
}
private void cleanUp() {
- if (mTargetTaskView == null) {
+ if (!enableAdditionalHomeAnimations() || mTargetTaskView == null) {
return;
}
RecentsView recentsView = mTargetTaskView.getRecentsView();
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 28fa81a..08bb6cd 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -100,6 +100,11 @@
TaskAnimationManager(Context ctx) {
mCtx = ctx;
}
+
+ SystemUiProxy getSystemUiProxy() {
+ return SystemUiProxy.INSTANCE.get(mCtx);
+ }
+
/**
* Preloads the recents animation.
*/
@@ -153,7 +158,7 @@
final BaseContainerInterface containerInterface = gestureState.getContainerInterface();
mLastGestureState = gestureState;
RecentsAnimationCallbacks newCallbacks = new RecentsAnimationCallbacks(
- SystemUiProxy.INSTANCE.get(mCtx), containerInterface.allowMinimizeSplitScreen());
+ getSystemUiProxy(), containerInterface.allowMinimizeSplitScreen());
mCallbacks = newCallbacks;
mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
@Override
@@ -260,7 +265,7 @@
}
RemoteAnimationTarget[] nonAppTargets = ENABLE_SHELL_TRANSITIONS
- ? null : SystemUiProxy.INSTANCE.get(mCtx).onStartingSplitLegacy(
+ ? null : getSystemUiProxy().onStartingSplitLegacy(
appearedTaskTargets);
if (nonAppTargets == null) {
nonAppTargets = new RemoteAnimationTarget[0];
@@ -327,12 +332,13 @@
if (ENABLE_SHELL_TRANSITIONS) {
final ActivityOptions options = ActivityOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
// Use regular (non-transient) launch for all apps page to control IME.
if (!containerInterface.allowAllAppsFromOverview()) {
options.setTransientLaunch();
}
options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
- mRecentsAnimationStartPending = SystemUiProxy.INSTANCE.get(mCtx)
+ mRecentsAnimationStartPending = getSystemUiProxy()
.startRecentsActivity(intent, options, mCallbacks);
if (enableHandleDelayedGestureCallbacks()) {
ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index fd141c3..f7e1b4e 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -228,6 +228,7 @@
// Take the thumbnail of the task without a scrim and apply it back after
float alpha = mThumbnailView.getDimAlpha();
+ // TODO(b/348643341) add ability to get override the scrim for this Bitmap retrieval
mThumbnailView.setDimAlpha(0);
Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index 758a737..eeacee1 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -259,7 +259,8 @@
return new Pair<>(translationX, translationY);
}
- bannerParams.gravity = BOTTOM | ((deviceProfile.isLandscape) ? START : CENTER_HORIZONTAL);
+ bannerParams.gravity =
+ BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL);
// Set correct width
if (desiredTaskId == splitBounds.leftTopTaskId) {
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 2836c89..dbe2b19 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -53,25 +53,31 @@
TaskThumbnailViewModel(
recentsView.mRecentsViewData,
(parent as TaskView).taskViewData,
+ (parent as TaskView).getTaskContainerForTaskThumbnailView(this)!!.taskContainerData,
recentsView.mTasksRepository,
)
}
private var uiState: TaskThumbnailUiState = Uninitialized
private var inheritedScale: Float = 1f
+ private var dimProgress: Float = 0f
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val scrimPaint = Paint().apply { color = Color.BLACK }
private val _measuredBounds = Rect()
private val measuredBounds: Rect
get() {
_measuredBounds.set(0, 0, measuredWidth, measuredHeight)
return _measuredBounds
}
+
private var cornerRadius: Float = TaskCornerRadius.get(context)
private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context)
constructor(context: Context?) : super(context)
+
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+
constructor(
context: Context?,
attrs: AttributeSet?,
@@ -87,6 +93,13 @@
invalidate()
}
}
+ MainScope().launch {
+ viewModel.dimProgress.collect { dimProgress ->
+ // TODO(b/348195366) Add fade in/out for scrim
+ this@TaskThumbnailView.dimProgress = dimProgress
+ invalidate()
+ }
+ }
MainScope().launch { viewModel.recentsFullscreenProgress.collect { invalidateOutline() } }
MainScope().launch {
viewModel.inheritedScale.collect { viewModelInheritedScale ->
@@ -111,6 +124,10 @@
is Snapshot -> drawSnapshotState(canvas, uiStateVal)
is BackgroundOnly -> drawBackgroundOnly(canvas, uiStateVal.backgroundColor)
}
+
+ if (dimProgress > 0) {
+ drawScrim(canvas)
+ }
}
private fun drawBackgroundOnly(canvas: Canvas, @ColorInt backgroundColor: Int) {
@@ -135,6 +152,11 @@
canvas.drawBitmap(snapshot.bitmap, snapshot.drawnRect, measuredBounds, null)
}
+ private fun drawScrim(canvas: Canvas) {
+ scrimPaint.alpha = (dimProgress * MAX_SCRIM_ALPHA).toInt()
+ canvas.drawRect(measuredBounds, scrimPaint)
+ }
+
private fun getCurrentCornerRadius() =
Utilities.mapRange(
viewModel.recentsFullscreenProgress.value,
@@ -145,5 +167,6 @@
companion object {
private val CLEAR_PAINT =
Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
+ private const val MAX_SCRIM_ALPHA = (0.4f * 255).toInt()
}
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
index 4511ea7..fe21174 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
@@ -25,6 +25,7 @@
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.systemui.shared.recents.model.Task
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -40,6 +41,7 @@
class TaskThumbnailViewModel(
recentsViewData: RecentsViewData,
taskViewData: TaskViewData,
+ taskContainerData: TaskContainerData,
private val tasksRepository: RecentTasksRepository,
) {
private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
@@ -50,6 +52,7 @@
combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
recentsScale * taskScale
}
+ val dimProgress: Flow<Float> = taskContainerData.taskMenuOpenProgress
val uiState: Flow<TaskThumbnailUiState> =
task
.flatMapLatest { taskFlow ->
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
new file mode 100644
index 0000000..769424c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class TaskContainerData {
+ val taskMenuOpenProgress = MutableStateFlow(0f)
+}
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
index cfa6b98..3140fff 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -40,6 +40,7 @@
SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED,
FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING,
INVALID_VELOCITY_ON_SWIPE_UP, RECENTS_ANIMATION_START_PENDING,
+ QUICK_SWITCH_FROM_HOME_FALLBACK, QUICK_SWITCH_FROM_HOME_FAILED,
/**
* These GestureEvents are specifically associated to state flags that get set in
@@ -282,6 +283,22 @@
+ " animation is still pending.",
writer);
break;
+ case QUICK_SWITCH_FROM_HOME_FALLBACK:
+ errorDetected |= printErrorIfTrue(
+ true,
+ prefix,
+ /* errorMessage= */ "Quick switch from home fallback case: the "
+ + "TaskView at the current page index was missing.",
+ writer);
+ break;
+ case QUICK_SWITCH_FROM_HOME_FAILED:
+ errorDetected |= printErrorIfTrue(
+ true,
+ prefix,
+ /* errorMessage= */ "Quick switch from home failed: the TaskViews at "
+ + "the current page index and index 0 were missing.",
+ writer);
+ break;
case EXPECTING_TASK_APPEARED:
case MOTION_DOWN:
case SET_END_TARGET:
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index e44f148..2b944bc 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -164,22 +164,7 @@
}
if (sourceRectHint.isEmpty()) {
- // Crop a Rect matches the aspect ratio and pivots at the center point.
- // To make the animation path simplified.
- if ((appBounds.width() / (float) appBounds.height()) > aspectRatio) {
- // use the full height.
- mSourceRectHint.set(0, 0,
- (int) (appBounds.height() * aspectRatio), appBounds.height());
- mSourceRectHint.offset(
- (appBounds.width() - mSourceRectHint.width()) / 2, 0);
- } else {
- // use the full width.
- mSourceRectHint.set(0, 0,
- appBounds.width(), (int) (appBounds.width() / aspectRatio));
- mSourceRectHint.offset(
- 0, (appBounds.height() - mSourceRectHint.height()) / 2);
- }
-
+ mSourceRectHint.set(getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio));
// Create a new overlay layer. We do not call detach on this instance, it's propagated
// to other classes like PipTaskOrganizer / RecentsAnimationController to complete
// the cleanup.
@@ -225,6 +210,26 @@
addOnUpdateListener(this::onAnimationUpdate);
}
+ /**
+ * Crop a Rect matches the aspect ratio and pivots at the center point.
+ */
+ private Rect getEnterPipWithOverlaySrcRectHint(Rect appBounds, float aspectRatio) {
+ final float appBoundsAspectRatio = appBounds.width() / (float) appBounds.height();
+ final int width, height;
+ int left = appBounds.left;
+ int top = appBounds.top;
+ if (appBoundsAspectRatio < aspectRatio) {
+ width = appBounds.width();
+ height = (int) (width / aspectRatio);
+ top = appBounds.top + (appBounds.height() - height) / 2;
+ } else {
+ height = appBounds.height();
+ width = (int) (height * aspectRatio);
+ left = appBounds.left + (appBounds.width() - width) / 2;
+ }
+ return new Rect(left, top, left + width, top + height);
+ }
+
private void onAnimationUpdate(RectF currentRect, float progress) {
if (mHasAnimationEnded) return;
final SurfaceControl.Transaction tx =
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 936f6a1..4c78e21 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -193,23 +193,23 @@
}
val taskContainer =
TaskContainer(
- task,
- // TODO(b/338360089): Support new TTV for DesktopTaskView
- thumbnailView = null,
- thumbnailViewDeprecated,
- iconView,
- TransformingTouchDelegate(iconView.asView()),
- SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
- digitalWellBeingToast = null,
- showWindowsView = null,
- taskOverlayFactory
- )
- .apply { thumbnailViewDeprecated.bind(task, overlay) }
+ task,
+ // TODO(b/338360089): Support new TTV for DesktopTaskView
+ thumbnailView = null,
+ thumbnailViewDeprecated,
+ iconView,
+ TransformingTouchDelegate(iconView.asView()),
+ SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+ digitalWellBeingToast = null,
+ showWindowsView = null,
+ taskOverlayFactory
+ )
if (index >= taskContainers.size) {
taskContainers.add(taskContainer)
} else {
taskContainers[index] = taskContainer
}
+ taskContainer.bind()
}
repeat(taskContainers.size - tasks.size) {
with(taskContainers.removeLast()) {
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index d6a3376..6296b0e 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -145,6 +145,8 @@
taskOverlayFactory
)
)
+ taskContainers.forEach { it.bind() }
+
this.splitBoundsConfig =
splitBoundsConfig?.also {
taskContainers[0]
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 4f802c9..d806e3d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -31,9 +31,11 @@
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.app.animation.Interpolators.OVERSHOOT_0_75;
import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
@@ -130,6 +132,7 @@
import androidx.core.graphics.ColorUtils;
import com.android.internal.jank.Cuj;
+import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
@@ -313,7 +316,6 @@
/**
* Can be used to tint the color of the RecentsView to simulate a scrim that can views
* excluded from. Really should be a proper scrim.
- * TODO(b/187528071): Remove this and replace with a real scrim.
*/
private static final FloatProperty<RecentsView> COLOR_TINT =
new FloatProperty<RecentsView>("colorTint") {
@@ -554,7 +556,6 @@
@Nullable
protected GestureState.GestureEndTarget mCurrentGestureEndTarget;
- // TODO(b/187528071): Remove these and replace with a real scrim.
private float mColorTint;
private final int mTintingColor;
@Nullable
@@ -2688,6 +2689,7 @@
}
private void animateRotation(int newRotation) {
+ AbstractFloatingView.closeAllOpenViewsExcept(mContainer, false, TYPE_REBIND_SAFE);
AnimatorSet pa = setRecentsChangedOrientation(true);
pa.addListener(AnimatorListeners.forSuccessCallback(() -> {
setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
@@ -4520,6 +4522,9 @@
* than the running task, when updating page offsets.
*/
public void setOffsetMidpointIndexOverride(int offsetMidpointIndexOverride) {
+ if (!enableAdditionalHomeAnimations()) {
+ return;
+ }
mOffsetMidpointIndexOverride = offsetMidpointIndexOverride;
updatePageOffsets();
}
@@ -6025,6 +6030,7 @@
* tasks to be dimmed while other elements in the recents view are left alone.
*/
public void showForegroundScrim(boolean show) {
+ // TODO(b/335606129) Add scrim response into new TTV - this is called from overlay
if (!show && mColorTint == 0) {
if (mTintingAnimator != null) {
mTintingAnimator.cancel();
@@ -6040,7 +6046,6 @@
}
/** Tint the RecentsView and TaskViews in to simulate a scrim. */
- // TODO(b/187528071): Replace this tinting with a scrim on top of RecentsView
private void setColorTint(float tintAmount) {
mColorTint = tintAmount;
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index eda58c5..4f446b2 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -18,6 +18,7 @@
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.launcher3.Flags.enableOverviewIconMenu;
+import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.quickstep.views.TaskThumbnailViewDeprecated.DIM_ALPHA;
@@ -367,6 +368,14 @@
mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA,
closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA),
ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
+ if (enableRefactorTaskThumbnail()) {
+ mRevealAnimator.addUpdateListener(animation -> {
+ float animatedFraction = animation.getAnimatedFraction();
+ float openProgress = closing ? (1 - animatedFraction) : animatedFraction;
+ mTaskContainer.getTaskContainerData()
+ .getTaskMenuOpenProgress().setValue(openProgress);
+ });
+ }
mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationStart(Animator animation) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 9c1aaa6..7a3b00f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -88,6 +88,7 @@
import com.android.quickstep.orientation.RecentsPagedOrientationHandler
import com.android.quickstep.task.thumbnail.TaskThumbnail
import com.android.quickstep.task.thumbnail.TaskThumbnailView
+import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.quickstep.util.ActiveGestureErrorDetector
import com.android.quickstep.util.ActiveGestureLog
@@ -667,6 +668,7 @@
taskOverlayFactory
)
)
+ taskContainers.forEach { it.bind() }
setOrientationState(orientedState)
}
@@ -693,24 +695,16 @@
}
val iconView = getOrInflateIconView(iconViewId)
return TaskContainer(
- task,
- thumbnailView,
- thumbnailViewDeprecated,
- iconView,
- TransformingTouchDelegate(iconView.asView()),
- stagePosition,
- DigitalWellBeingToast(container, this),
- findViewById(showWindowViewId)!!,
- taskOverlayFactory
- )
- .apply {
- if (enableRefactorTaskThumbnail()) {
- thumbnailViewDeprecated.setTaskOverlay(overlay)
- bindThumbnailView()
- } else {
- thumbnailViewDeprecated.bind(task, overlay)
- }
- }
+ task,
+ thumbnailView,
+ thumbnailViewDeprecated,
+ iconView,
+ TransformingTouchDelegate(iconView.asView()),
+ stagePosition,
+ DigitalWellBeingToast(container, this),
+ findViewById(showWindowViewId)!!,
+ taskOverlayFactory
+ )
}
protected fun getOrInflateIconView(@IdRes iconViewId: Int): TaskViewIcon {
@@ -1379,7 +1373,6 @@
open fun setColorTint(amount: Float, tintColor: Int) {
taskContainers.forEach {
if (!enableRefactorTaskThumbnail()) {
- // TODO(b/334832108) Add scrim to new TTV
it.thumbnailViewDeprecated.dimAlpha = amount
}
it.iconView.setIconColorTint(tintColor, amount)
@@ -1522,6 +1515,9 @@
resetViewTransforms()
}
+ fun getTaskContainerForTaskThumbnailView(taskThumbnailView: TaskThumbnailView): TaskContainer? =
+ taskContainers.firstOrNull { it.thumbnailView == taskThumbnailView }
+
open fun resetViewTransforms() {
// fullscreenTranslation and accumulatedTranslation should not be reset, as
// resetViewTransforms is called during QuickSwitch scrolling.
@@ -1623,6 +1619,7 @@
taskOverlayFactory: TaskOverlayFactory
) {
val overlay: TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
+ val taskContainerData = TaskContainerData()
val snapshotView: View
get() = thumbnailView ?: thumbnailViewDeprecated
@@ -1656,6 +1653,15 @@
thumbnailView?.let { taskView.removeView(it) }
}
+ fun bind() {
+ if (enableRefactorTaskThumbnail() && thumbnailView != null) {
+ thumbnailViewDeprecated.setTaskOverlay(overlay)
+ bindThumbnailView()
+ } else {
+ thumbnailViewDeprecated.bind(task, overlay)
+ }
+ }
+
// TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
// so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
fun bindThumbnailView() {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
index c09dcf2..fe4e2d2 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
@@ -16,6 +16,7 @@
package com.android.launcher3.taskbar.allapps
+import android.animation.AnimatorTestRule
import android.content.ComponentName
import android.content.Intent
import android.os.Process
@@ -42,6 +43,7 @@
class TaskbarAllAppsControllerTest {
@get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
@InjectController lateinit var allAppsController: TaskbarAllAppsController
@InjectController lateinit var overlayController: TaskbarOverlayController
@@ -166,6 +168,21 @@
assertThat(btv.hasDot()).isTrue()
}
+ @Test
+ fun testToggleSearch_searchEditTextFocused() {
+ getInstrumentation().runOnMainSync { allAppsController.toggleSearch() }
+ getInstrumentation().runOnMainSync {
+ // All Apps is now attached to window. Open animation is posted but not started.
+ }
+
+ getInstrumentation().runOnMainSync {
+ // Animation has started. Advance to end of animation.
+ animatorTestRule.advanceTimeBy(overlayController.openDuration.toLong())
+ }
+ val editText = overlayController.requestWindow().appsView.searchUiManager.editText
+ assertThat(editText?.hasFocus()).isTrue()
+ }
+
private companion object {
private val TEST_APPS =
Array(16) {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
new file mode 100644
index 0000000..20bd617
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.animation
+
+import androidx.core.animation.AnimatorTestRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleAnimatorTest {
+
+ @get:Rule val animatorTestRule = AnimatorTestRule()
+
+ private lateinit var bubbleAnimator: BubbleAnimator
+
+ @Test
+ fun animateNewBubble_isRunning() {
+ bubbleAnimator =
+ BubbleAnimator(
+ iconSize = 40f,
+ expandedBarIconSpacing = 10f,
+ bubbleCount = 5,
+ onLeft = false
+ )
+ val listener = TestBubbleAnimatorListener()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleAnimator.animateNewBubble(selectedBubbleIndex = 2, listener)
+ }
+
+ assertThat(bubbleAnimator.isRunning).isTrue()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+ assertThat(bubbleAnimator.isRunning).isFalse()
+ }
+
+ @Test
+ fun animateRemovedBubble_isRunning() {
+ bubbleAnimator =
+ BubbleAnimator(
+ iconSize = 40f,
+ expandedBarIconSpacing = 10f,
+ bubbleCount = 5,
+ onLeft = false
+ )
+ val listener = TestBubbleAnimatorListener()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleAnimator.animateRemovedBubble(
+ bubbleIndex = 2,
+ selectedBubbleIndex = 3,
+ removingLastBubble = false,
+ listener
+ )
+ }
+
+ assertThat(bubbleAnimator.isRunning).isTrue()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+ assertThat(bubbleAnimator.isRunning).isFalse()
+ }
+
+ private class TestBubbleAnimatorListener : BubbleAnimator.Listener {
+
+ override fun onAnimationUpdate(animatedFraction: Float) {}
+
+ override fun onAnimationCancel() {}
+
+ override fun onAnimationEnd() {}
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index cc579ab..e9c0dd6 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -24,6 +24,7 @@
import android.view.View
import android.view.View.VISIBLE
import android.widget.FrameLayout
+import androidx.core.animation.AnimatorTestRule
import androidx.core.graphics.drawable.toBitmap
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.test.core.app.ApplicationProvider
@@ -41,6 +42,7 @@
import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
import com.google.common.truth.Truth.assertThat
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
@@ -53,6 +55,8 @@
@RunWith(AndroidJUnit4::class)
class BubbleBarViewAnimatorTest {
+ @get:Rule val animatorTestRule = AnimatorTestRule()
+
private val context = ApplicationProvider.getApplicationContext<Context>()
private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler
private lateinit var overflowView: BubbleView
@@ -380,6 +384,46 @@
verify(bubbleStashController).showBubbleBarImmediate()
}
+ @Test
+ fun animateBubbleBarForCollapsed() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+ whenever(bubbleStashController.bubbleBarTranslationY)
+ .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+ val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleBarForCollapsed(bubble)
+ }
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ // verify we started animating
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+ // advance the animation handler by the duration of the initial lift
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+
+ // the lift animation is complete; the spring back animation should start now
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ barAnimator.assertIsRunning()
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ // the bubble bar translation y should be back to its initial value
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+ verify(bubbleStashController).showBubbleBarImmediate()
+ }
+
private fun setUpBubbleBar() {
bubbleBarView = BubbleBarView(context)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
index 3b8754c..a394b65 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
@@ -28,6 +28,7 @@
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -43,9 +44,10 @@
class TaskThumbnailViewModelTest {
private val recentsViewData = RecentsViewData()
private val taskViewData = TaskViewData()
+ private val taskContainerData = TaskContainerData()
private val tasksRepository = FakeTasksRepository()
private val systemUnderTest =
- TaskThumbnailViewModel(recentsViewData, taskViewData, tasksRepository)
+ TaskThumbnailViewModel(recentsViewData, taskViewData, taskContainerData, tasksRepository)
private val tasks = (0..5).map(::createTaskWithId)
diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
index 5c7b4ab..039dce4 100644
--- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
@@ -34,7 +34,7 @@
import com.android.launcher3.model.WidgetPredictionsRequester.filterPredictions
import com.android.launcher3.model.WidgetPredictionsRequester.notOnUiSurfaceFilter
import com.android.launcher3.util.ActivityContextWrapper
-import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
import com.google.common.truth.Truth.assertThat
@@ -62,7 +62,7 @@
private lateinit var widgetItem1b: WidgetItem
private lateinit var widgetItem2: WidgetItem
- private lateinit var allWidgets: Map<PackageUserKey, List<WidgetItem>>
+ private lateinit var allWidgets: Map<ComponentKey, WidgetItem>
@Mock private lateinit var iconCache: IconCache
@@ -93,9 +93,9 @@
allWidgets =
mapOf(
- PackageUserKey(APP_1_PACKAGE_NAME, mUserHandle) to
- listOf(widgetItem1a, widgetItem1b),
- PackageUserKey(APP_2_PACKAGE_NAME, mUserHandle) to listOf(widgetItem2),
+ ComponentKey(widgetItem1a.componentName, widgetItem1a.user) to widgetItem1a,
+ ComponentKey(widgetItem1b.componentName, widgetItem1b.user) to widgetItem1b,
+ ComponentKey(widgetItem2.componentName, widgetItem2.user) to widgetItem2,
)
}
@@ -156,7 +156,7 @@
}
@Test
- fun filterPredictions_appPredictions_returnsWidgetFromPackage() {
+ fun filterPredictions_appPredictions_returnsEmptyList() {
val widgetsAlreadyOnSurface = arrayListOf(widget1bInfo)
val filter: Predicate<WidgetItem> = notOnUiSurfaceFilter(widgetsAlreadyOnSurface)
@@ -176,8 +176,7 @@
),
)
- assertThat(filterPredictions(predictions, allWidgets, filter))
- .containsExactly(widgetItem1a, widgetItem2)
+ assertThat(filterPredictions(predictions, allWidgets, filter)).isEmpty()
}
private fun createWidgetItem(
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 104263a..486dc68 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -16,24 +16,35 @@
package com.android.launcher3.taskbar
-import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.ComponentName
import android.content.Intent
import android.os.Process
import android.os.UserHandle
import android.testing.AndroidTestingRunner
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.quickstep.RecentsModel
+import com.android.quickstep.RecentsModel.RecentTasksChangedListener
+import com.android.quickstep.TaskIconCache
+import com.android.quickstep.util.DesktopTask
+import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.Task
import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@RunWith(AndroidTestingRunner::class)
@@ -41,177 +52,471 @@
@get:Rule val mockitoRule = MockitoJUnit.rule()
+ @Mock private lateinit var mockIconCache: TaskIconCache
@Mock private lateinit var mockRecentsModel: RecentsModel
@Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
private var nextTaskId: Int = 500
+ private var taskListChangeId: Int = 1
private lateinit var recentAppsController: TaskbarRecentAppsController
+ private lateinit var recentTasksChangedListener: RecentTasksChangedListener
private lateinit var userHandle: UserHandle
@Before
fun setUp() {
super.setup()
userHandle = Process.myUserHandle()
+
+ whenever(mockRecentsModel.iconCache).thenReturn(mockIconCache)
recentAppsController =
TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController }
recentAppsController.init(taskbarControllers)
- recentAppsController.isEnabled = true
- recentAppsController.setApps(
- ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray()
- )
+ recentAppsController.canShowRunningApps = true
+ recentAppsController.canShowRecentApps = true
+
+ val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java)
+ verify(mockRecentsModel).registerRecentTasksChangedListener(listenerCaptor.capture())
+ recentTasksChangedListener = listenerCaptor.value
}
@Test
- fun updateHotseatItemInfos_notInDesktopMode_returnsExistingHotseatItems() {
- setInDesktopMode(false)
- val hotseatItems =
- createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
-
- assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()))
- .isEqualTo(hotseatItems.toTypedArray())
- }
-
- @Test
- fun updateHotseatItemInfos_notInDesktopMode_runningApps_returnsExistingHotseatItems() {
- setInDesktopMode(false)
- val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
- val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
- val runningTasks =
- createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
-
+ fun updateHotseatItemInfos_cantShowRunning_inDesktopMode_returnsAllHotseatItems() {
+ recentAppsController.canShowRunningApps = false
+ setInDesktopMode(true)
+ val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1)
val newHotseatItems =
- recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
-
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = hotseatPackages,
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = emptyList()
+ )
assertThat(newHotseatItems.map { it?.targetPackage })
.containsExactlyElementsIn(hotseatPackages)
}
@Test
- fun updateHotseatItemInfos_noRunningApps_returnsExistingHotseatItems() {
- setInDesktopMode(true)
- val hotseatItems =
- createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
-
- assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()))
- .isEqualTo(hotseatItems.toTypedArray())
- }
-
- @Test
- fun updateHotseatItemInfos_returnsExistingHotseatItemsAndRunningApps() {
- setInDesktopMode(true)
- val hotseatItems =
- createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
- val runningTasks =
- createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
-
- val newHotseatItems =
- recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
-
- val expectedPackages =
- listOf(
- HOTSEAT_PACKAGE_1,
- HOTSEAT_PACKAGE_2,
- RUNNING_APP_PACKAGE_1,
- RUNNING_APP_PACKAGE_2,
- )
- assertThat(newHotseatItems.map { it?.targetPackage })
- .containsExactlyElementsIn(expectedPackages)
- }
-
- @Test
- fun updateHotseatItemInfos_runningAppIsHotseatItem_returnsDistinctItems() {
- setInDesktopMode(true)
- val hotseatItems =
- createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
- val runningTasks =
- createDesktopTasksFromPackageNames(
- listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
- )
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
-
- val newHotseatItems =
- recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
-
- val expectedPackages =
- listOf(
- HOTSEAT_PACKAGE_1,
- HOTSEAT_PACKAGE_2,
- RUNNING_APP_PACKAGE_1,
- RUNNING_APP_PACKAGE_2,
- )
- assertThat(newHotseatItems.map { it?.targetPackage })
- .containsExactlyElementsIn(expectedPackages)
- }
-
- @Test
- fun getRunningApps_notInDesktopMode_returnsEmptySet() {
+ fun updateHotseatItemInfos_cantShowRecent_notInDesktopMode_returnsAllHotseatItems() {
+ recentAppsController.canShowRecentApps = false
setInDesktopMode(false)
- val runningTasks =
- createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
-
- assertThat(recentAppsController.runningApps).isEmpty()
- assertThat(recentAppsController.minimizedApps).isEmpty()
+ val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1)
+ val newHotseatItems =
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = hotseatPackages,
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = emptyList()
+ )
+ assertThat(newHotseatItems.map { it?.targetPackage })
+ .containsExactlyElementsIn(hotseatPackages)
}
@Test
- fun getRunningApps_inDesktopMode_returnsRunningApps() {
+ fun updateHotseatItemInfos_canShowRunning_inDesktopMode_returnsNonPredictedHotseatItems() {
+ recentAppsController.canShowRunningApps = true
setInDesktopMode(true)
- val runningTasks =
- createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
+ val newHotseatItems =
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = emptyList()
+ )
+ val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
+ assertThat(newHotseatItems.map { it?.targetPackage })
+ .containsExactlyElementsIn(expectedPackages)
+ }
- assertThat(recentAppsController.runningApps)
- .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
- assertThat(recentAppsController.minimizedApps).isEmpty()
+ @Test
+ fun updateHotseatItemInfos_canShowRecent_notInDesktopMode_returnsNonPredictedHotseatItems() {
+ recentAppsController.canShowRecentApps = true
+ setInDesktopMode(false)
+ val newHotseatItems =
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = emptyList()
+ )
+ val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
+ assertThat(newHotseatItems.map { it?.targetPackage })
+ .containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_cantShowRunning_inDesktopMode_shownTasks_returnsEmptyList() {
+ recentAppsController.canShowRunningApps = false
+ setInDesktopMode(true)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
+ runningTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.shownTasks).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_cantShowRecent_notInDesktopMode_shownTasks_returnsEmptyList() {
+ recentAppsController.canShowRecentApps = false
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ assertThat(recentAppsController.shownTasks).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_noRecentTasks_shownTasks_returnsEmptyList() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.shownTasks).isEmpty()
+ assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_noRunningApps_shownTasks_returnsEmptyList() {
+ setInDesktopMode(true)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ assertThat(recentAppsController.shownTasks).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_shownTasks_returnsRunningTasks() {
+ setInDesktopMode(true)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = emptyList()
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_runningAppIsHotseatItem_shownTasks_returnsDistinctItems() {
+ setInDesktopMode(true)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+ runningTaskPackages =
+ listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ val expectedPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_getRunningApps_returnsEmptySet() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.runningAppPackages).isEmpty()
+ assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_getRunningApps_returnsAllDesktopTasks() {
+ setInDesktopMode(true)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.runningAppPackages)
+ .containsExactlyElementsIn(runningTaskPackages)
+ assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_getRunningApps_includesHotseat() {
+ setInDesktopMode(true)
+ val runningTaskPackages =
+ listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ assertThat(recentAppsController.runningAppPackages)
+ .containsExactlyElementsIn(runningTaskPackages)
+ assertThat(recentAppsController.minimizedAppPackages).isEmpty()
}
@Test
fun getMinimizedApps_inDesktopMode_returnsAllAppsRunningAndInvisibleAppsMinimized() {
setInDesktopMode(true)
- val runningTasks =
- ArrayList(
- listOf(
- createDesktopTaskInfo(RUNNING_APP_PACKAGE_1) { isVisible = true },
- createDesktopTaskInfo(RUNNING_APP_PACKAGE_2) { isVisible = true },
- createDesktopTaskInfo(RUNNING_APP_PACKAGE_3) { isVisible = false },
- )
- )
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
+ val runningTaskPackages =
+ listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
+ val minimizedTaskIndices = setOf(2) // RUNNING_APP_PACKAGE_3
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ minimizedTaskIndices = minimizedTaskIndices,
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.runningAppPackages)
+ .containsExactlyElementsIn(runningTaskPackages)
+ assertThat(recentAppsController.minimizedAppPackages).containsExactly(RUNNING_APP_PACKAGE_3)
+ }
- assertThat(recentAppsController.runningApps)
- .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
- assertThat(recentAppsController.minimizedApps).containsExactly(RUNNING_APP_PACKAGE_3)
+ @Test
+ fun getMinimizedApps_inDesktopMode_twoTasksSamePackageOneMinimizedReturnsNotMinimized() {
+ setInDesktopMode(true)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_1)
+ val minimizedTaskIndices = setOf(1) // The second RUNNING_APP_PACKAGE_1 task.
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ minimizedTaskIndices = minimizedTaskIndices,
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.runningAppPackages)
+ .containsExactlyElementsIn(runningTaskPackages.toSet())
+ assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_shownTasks_maintainsOrder() {
+ setInDesktopMode(true)
+ val originalOrder = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = originalOrder,
+ recentTaskPackages = emptyList()
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+ recentTaskPackages = emptyList()
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ assertThat(shownPackages).isEqualTo(originalOrder)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_shownTasks_maintainsRecency() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Most recent packages, minus the currently running one (RECENT_PACKAGE_1).
+ assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3))
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_addTask_shownTasks_maintainsOrder() {
+ setInDesktopMode(true)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages =
+ listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_3),
+ recentTaskPackages = emptyList()
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ val expectedOrder =
+ listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
+ assertThat(shownPackages).isEqualTo(expectedOrder)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_addTask_shownTasks_maintainsRecency() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_3, RECENT_PACKAGE_2)
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Most recent packages, minus the currently running one (RECENT_PACKAGE_1).
+ assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3))
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_removeTask_shownTasks_maintainsOrder() {
+ setInDesktopMode(true)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages =
+ listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3),
+ recentTaskPackages = emptyList()
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+ recentTaskPackages = emptyList()
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ assertThat(shownPackages).isEqualTo(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_removeTask_shownTasks_maintainsRecency() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Most recent packages, minus the currently running one (RECENT_PACKAGE_3).
+ assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2))
+ }
+
+ @Test
+ fun onRecentTasksChanged_enterDesktopMode_shownTasks_onlyIncludesRunningTasks() {
+ setInDesktopMode(false)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = recentTaskPackages
+ )
+ setInDesktopMode(true)
+ recentTasksChangedListener.onRecentTasksChanged()
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_exitDesktopMode_shownTasks_onlyIncludesRecentTasks() {
+ setInDesktopMode(true)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = recentTaskPackages
+ )
+ setInDesktopMode(false)
+ recentTasksChangedListener.onRecentTasksChanged()
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Don't expect RECENT_PACKAGE_3 because it is currently running.
+ val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_hasRecentTasks_shownTasks_returnsRecentTasks() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // RECENT_PACKAGE_3 is the top task (visible to user) so should be excluded.
+ val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_hasRecentAndRunningTasks_shownTasks_returnsRecentTaskAndDesktopTile() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
+ // Only 2 recent tasks shown: Desktop Tile + 1 Recent Task
+ val desktopTilePackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1)
+ val expectedPackages = listOf(desktopTilePackages, recentTaskPackages)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_hasRecentAndSplitTasks_shownTasks_returnsRecentTaskAndPair() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_SPLIT_PACKAGES_1, RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
+ // Only 2 recent tasks shown: Pair + 1 Recent Task
+ val pairPackages = RECENT_SPLIT_PACKAGES_1.split("_")
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1)
+ val expectedPackages = listOf(pairPackages, recentTaskPackages)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ private fun prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages: List<String>,
+ runningTaskPackages: List<String>,
+ minimizedTaskIndices: Set<Int> = emptySet(),
+ recentTaskPackages: List<String>,
+ ): Array<ItemInfo?> {
+ val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
+ val newHotseatItems =
+ recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
+ val runningTasks = createDesktopTask(runningTaskPackages, minimizedTaskIndices)
+ val recentTasks = createRecentTasksFromPackageNames(recentTaskPackages)
+ val allTasks =
+ ArrayList<GroupTask>().apply {
+ if (runningTasks != null) {
+ add(runningTasks)
+ }
+ addAll(recentTasks)
+ }
+ doAnswer {
+ val callback: Consumer<ArrayList<GroupTask>> = it.getArgument(0)
+ callback.accept(allTasks)
+ taskListChangeId
+ }
+ .whenever(mockRecentsModel)
+ .getTasks(any<Consumer<List<GroupTask>>>())
+ recentTasksChangedListener.onRecentTasksChanged()
+ return newHotseatItems
}
private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
- return packageNames.map { createTestAppInfo(packageName = it) }
- }
-
- private fun createDesktopTasksFromPackageNames(
- packageNames: List<String>
- ): ArrayList<RunningTaskInfo> {
- return ArrayList(packageNames.map { createDesktopTaskInfo(packageName = it) })
- }
-
- private fun createDesktopTaskInfo(
- packageName: String,
- init: RunningTaskInfo.() -> Unit = { isVisible = true },
- ): RunningTaskInfo {
- return RunningTaskInfo().apply {
- taskId = nextTaskId++
- configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
- realActivity = ComponentName(packageName, "TestActivity")
- init()
+ return packageNames.map {
+ createTestAppInfo(packageName = it).apply {
+ container =
+ if (it.startsWith("predicted")) {
+ CONTAINER_HOTSEAT_PREDICTION
+ } else {
+ CONTAINER_HOTSEAT
+ }
+ }
}
}
@@ -220,23 +525,67 @@
className: String = "testClassName"
) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
+ private fun createDesktopTask(
+ packageNames: List<String>,
+ minimizedTaskIndices: Set<Int>
+ ): DesktopTask? {
+ if (packageNames.isEmpty()) return null
+
+ return DesktopTask(
+ ArrayList(
+ packageNames.mapIndexed { index, packageName ->
+ createTask(packageName, index !in minimizedTaskIndices)
+ }
+ )
+ )
+ }
+
+ private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
+ return packageNames.map {
+ if (it.startsWith("split")) {
+ val splitPackages = it.split("_")
+ GroupTask(
+ createTask(splitPackages[0]),
+ createTask(splitPackages[1]),
+ /* splitBounds = */ null
+ )
+ } else {
+ GroupTask(createTask(it))
+ }
+ }
+ }
+
+ private fun createTask(packageName: String, isVisible: Boolean = true): Task {
+ return Task(
+ Task.TaskKey(
+ nextTaskId++,
+ WINDOWING_MODE_FREEFORM,
+ Intent().apply { `package` = packageName },
+ ComponentName(packageName, "TestActivity"),
+ userHandle.identifier,
+ 0
+ )
+ )
+ .apply { this.isVisible = isVisible }
+ }
+
private fun setInDesktopMode(inDesktopMode: Boolean) {
whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode)
}
+ private val GroupTask.packageNames: List<String>
+ get() = tasks.map { task -> task.key.packageName }
+
private companion object {
const val HOTSEAT_PACKAGE_1 = "hotseat1"
const val HOTSEAT_PACKAGE_2 = "hotseat2"
+ const val PREDICTED_PACKAGE_1 = "predicted1"
const val RUNNING_APP_PACKAGE_1 = "running1"
const val RUNNING_APP_PACKAGE_2 = "running2"
const val RUNNING_APP_PACKAGE_3 = "running3"
- val ALL_APP_PACKAGES =
- listOf(
- HOTSEAT_PACKAGE_1,
- HOTSEAT_PACKAGE_2,
- RUNNING_APP_PACKAGE_1,
- RUNNING_APP_PACKAGE_2,
- RUNNING_APP_PACKAGE_3,
- )
+ const val RECENT_PACKAGE_1 = "recent1"
+ const val RECENT_PACKAGE_2 = "recent2"
+ const val RECENT_PACKAGE_3 = "recent3"
+ const val RECENT_SPLIT_PACKAGES_1 = "split1_split2"
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 7877e8a..1dfab26 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -401,6 +401,7 @@
@Test
@NavigationModeSwitch
@PortraitLandscape
+ @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406
public void testQuickSwitchFromHome() throws Exception {
startTestActivity(2);
mLauncher.goHome().quickSwitchToPreviousApp();
diff --git a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
new file mode 100644
index 0000000..2d79623
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+public class TaskAnimationManagerTest {
+
+ @Mock
+ private Context mContext;
+
+ @Mock
+ private SystemUiProxy mSystemUiProxy;
+
+ private TaskAnimationManager mTaskAnimationManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTaskAnimationManager = new TaskAnimationManager(mContext) {
+ @Override
+ SystemUiProxy getSystemUiProxy() {
+ return mSystemUiProxy;
+ }
+ };
+ }
+
+ @Test
+ public void startRecentsActivity_allowBackgroundLaunch() {
+ assumeTrue(TaskAnimationManager.ENABLE_SHELL_TRANSITIONS);
+
+ final LauncherActivityInterface activityInterface = mock(LauncherActivityInterface.class);
+ final GestureState gestureState = mock(GestureState.class);
+ final RecentsAnimationCallbacks.RecentsAnimationListener listener =
+ mock(RecentsAnimationCallbacks.RecentsAnimationListener.class);
+ doReturn(activityInterface).when(gestureState).getContainerInterface();
+ mTaskAnimationManager.startRecentsAnimation(gestureState, new Intent(), listener);
+
+ final ArgumentCaptor<ActivityOptions> optionsCaptor =
+ ArgumentCaptor.forClass(ActivityOptions.class);
+ verify(mSystemUiProxy).startRecentsActivity(any(), optionsCaptor.capture(), any());
+ assertTrue(optionsCaptor.getValue()
+ .isPendingIntentBackgroundActivityLaunchAllowedByPermission());
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt b/quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
index b637e7d..0bf68eb 100644
--- a/quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
@@ -35,13 +35,22 @@
private val taskbarSpecsEvaluator = spy(TaskbarSpecsEvaluator(taskbarFeatureEvaluator))
@Test
- fun testGetIconSizeByGrid_whenTaskbarIsTransient_withValidRowAndColumn() {
+ fun testGetIconSizeByGrid_whenTaskbarIsTransient_withValidRowAndColumnInLandscape() {
doReturn(true).whenever(taskbarFeatureEvaluator).isTransient
- assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(6, 5))
+ doReturn(true).whenever(taskbarFeatureEvaluator).isLandscape
+ assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(4, 4))
.isEqualTo(TaskbarIconSpecs.iconSize52dp)
}
@Test
+ fun testGetIconSizeByGrid_whenTaskbarIsTransient_withValidRowAndColumnInPortrait() {
+ doReturn(true).whenever(taskbarFeatureEvaluator).isTransient
+ doReturn(false).whenever(taskbarFeatureEvaluator).isLandscape
+ assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(4, 4))
+ .isEqualTo(TaskbarIconSpecs.iconSize48dp)
+ }
+
+ @Test
fun testGetIconSizeByGrid_whenTaskbarIsTransient_withInvalidRowAndColumn() {
doReturn(true).whenever(taskbarFeatureEvaluator).isTransient
assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(1, 2))
diff --git a/res/layout/bubble_bar_overflow_button.xml b/res/layout/bubble_bar_overflow_button.xml
new file mode 100644
index 0000000..cb54990
--- /dev/null
+++ b/res/layout/bubble_bar_overflow_button.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<com.android.launcher3.taskbar.bubbles.BubbleView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/bubble_overflow_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
\ No newline at end of file
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index e9f951b..08ff6e7 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -27,8 +27,7 @@
<string name="safemode_widget_error" msgid="4863470563535682004">"Widgets disabled in Safe mode"</string>
<string name="shortcut_not_available" msgid="2536503539825726397">"Shortcut isn\'t available"</string>
<string name="home_screen" msgid="5629429142036709174">"Home"</string>
- <!-- no translation found for set_default_home_app (5808906607627586381) -->
- <skip />
+ <string name="set_default_home_app" msgid="5808906607627586381">"Set <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> as default home app in Settings"</string>
<string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
<string name="split_app_info_accessibility" msgid="5475288491241414932">"App info for %1$s"</string>
<string name="split_app_usage_settings" msgid="7214375263347964093">"Usage settings for %1$s"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index fb08c7e..fa6d1f1 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -27,8 +27,7 @@
<string name="safemode_widget_error" msgid="4863470563535682004">"Widgets disabled in Safe mode"</string>
<string name="shortcut_not_available" msgid="2536503539825726397">"Shortcut isn\'t available"</string>
<string name="home_screen" msgid="5629429142036709174">"Home"</string>
- <!-- no translation found for set_default_home_app (5808906607627586381) -->
- <skip />
+ <string name="set_default_home_app" msgid="5808906607627586381">"Set <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> as default home app in Settings"</string>
<string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
<string name="split_app_info_accessibility" msgid="5475288491241414932">"App info for %1$s"</string>
<string name="split_app_usage_settings" msgid="7214375263347964093">"Usage settings for %1$s"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index c235c3c..b287b2f 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -86,7 +86,7 @@
<string name="remove_drop_target_label" msgid="7812859488053230776">"ਹਟਾਓ"</string>
<string name="uninstall_drop_target_label" msgid="4722034217958379417">"ਅਣਸਥਾਪਤ ਕਰੋ"</string>
<string name="app_info_drop_target_label" msgid="692894985365717661">"ਐਪ ਜਾਣਕਾਰੀ"</string>
- <string name="install_private_system_shortcut_label" msgid="1616889277073184841">"ਨਿੱਜੀ ਵਜੋਂ ਸਥਾਪਤ ਕਰੋ"</string>
+ <string name="install_private_system_shortcut_label" msgid="1616889277073184841">"ਪ੍ਰਾਈਵੇਟ ਵਜੋਂ ਸਥਾਪਤ ਕਰੋ"</string>
<string name="uninstall_private_system_shortcut_label" msgid="8423460530441627982">"ਐਪ ਅਣਸਥਾਪਤ ਕਰੋ"</string>
<string name="install_drop_target_label" msgid="2539096853673231757">"ਸਥਾਪਤ ਕਰੋ"</string>
<string name="dismiss_prediction_label" msgid="3357562989568808658">"ਐਪ ਦਾ ਸੁਝਾਅ ਨਾ ਦਿਓ"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index b2ec45d..4c4b13b 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -192,7 +192,7 @@
<string name="ps_container_settings" msgid="6059734123353320479">"Configurações do Espaço particular"</string>
<string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"Privada, desbloqueado."</string>
<string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"Privada, bloqueado."</string>
- <string name="ps_container_lock_title" msgid="2640257399982364682">"Bloquear"</string>
+ <string name="ps_container_lock_title" msgid="2640257399982364682">"Bloqueio"</string>
<string name="ps_container_transition" msgid="8667331812048014412">"Espaço particular em transição"</string>
<string name="ps_add_button_label" msgid="8127988716897128773">"Instalar"</string>
<string name="ps_add_button_content_description" msgid="3254274107740952556">"Instalar apps no espaço privado"</string>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 7d09164..83427a0 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -430,10 +430,21 @@
setDownloadStateContentDescription(info, info.getProgressLevel());
}
+ /**
+ * Directly set the icon and label.
+ */
+ @UiThread
+ public void applyIconAndLabel(Drawable icon, CharSequence label) {
+ applyCompoundDrawables(icon);
+ setText(label);
+ setContentDescription(label);
+ }
+
/** Updates whether the app this view represents is currently running. */
@UiThread
public void updateRunningState(RunningAppState runningAppState) {
mRunningAppState = runningAppState;
+ invalidate();
}
protected void setItemInfo(ItemInfoWithIcon itemInfo) {
@@ -1291,13 +1302,4 @@
public boolean canShowLongPressPopup() {
return getTag() instanceof ItemInfo && ShortcutUtil.supportsShortcuts((ItemInfo) getTag());
}
-
- /** Returns the package name of the app this icon represents. */
- public String getTargetPackageName() {
- Object tag = getTag();
- if (tag instanceof ItemInfo itemInfo) {
- return itemInfo.getTargetPackage();
- }
- return null;
- }
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 4e566ab..d905801 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -2797,9 +2797,11 @@
}
private void updateDisallowBack() {
- if (BuildCompat.isAtLeastV() && Flags.enableDesktopWindowingMode()
- && mDeviceProfile.isTablet) {
- // TODO(b/330183377) disable back in launcher when when we productionize
+ if (BuildCompat.isAtLeastV()
+ && Flags.enableDesktopWindowingMode()
+ && !Flags.enableDesktopWindowingWallpaperActivity()
+ && mDeviceProfile.isTablet) {
+ // TODO(b/333533253): Clean up after desktop wallpaper activity flag is rolled out
return;
}
LauncherRootView rv = getRootView();
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 33e6f91..d0596fa 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.config.FeatureFlags.BooleanFlag.DISABLED;
import static com.android.launcher3.config.FeatureFlags.BooleanFlag.ENABLED;
import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification;
+import static com.android.wm.shell.Flags.enableTaskbarOnPhones;
import android.content.res.Resources;
@@ -143,7 +144,7 @@
DISABLED, "Sends a notification whenever launcher encounters an uncaught exception.");
public static final boolean ENABLE_TASKBAR_NAVBAR_UNIFICATION =
- enableTaskbarNavbarUnification() && !isPhone();
+ enableTaskbarNavbarUnification() && (!isPhone() || enableTaskbarOnPhones());
private static boolean isPhone() {
final boolean isPhone;
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index dc6968c..312c6f4 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -209,7 +209,7 @@
mApp.getContext().getContentResolver(),
"launcher_broadcast_installed_apps",
/* def= */ 0);
- if (launcherBroadcastInstalledApps == 1) {
+ if (launcherBroadcastInstalledApps == 1 && mIsRestoreFromBackup) {
List<FirstScreenBroadcastModel> broadcastModels =
FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
mPmHelper,
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 16fabe2..7f36d6f 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -182,6 +182,11 @@
return INSTANCE.get(context).getInfo().isTransientTaskbar();
}
+ /** Returns whether we are currently in Desktop mode. */
+ public static boolean isInDesktopMode(Context context) {
+ return INSTANCE.get(context).getInfo().isInDesktopMode();
+ }
+
/**
* Handles info change for desktop mode.
*/
diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java
index ccd154a..cd6701d 100644
--- a/src/com/android/launcher3/util/SettingsCache.java
+++ b/src/com/android/launcher3/util/SettingsCache.java
@@ -18,6 +18,8 @@
import static android.provider.Settings.System.ACCELEROMETER_ROTATION;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
@@ -87,7 +89,7 @@
@Override
public void close() {
- mResolver.unregisterContentObserver(this);
+ UI_HELPER_EXECUTOR.execute(() -> mResolver.unregisterContentObserver(this));
}
@Override
@@ -135,7 +137,8 @@
CopyOnWriteArrayList<OnChangeListener> l = new CopyOnWriteArrayList<>();
l.add(changeListener);
mListenerMap.put(uri, l);
- mResolver.registerContentObserver(uri, false, this);
+ UI_HELPER_EXECUTOR.execute(
+ () -> mResolver.registerContentObserver(uri, false, this));
}
}
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index 325c1cd..f90a3e4 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -16,6 +16,7 @@
package com.android.launcher3.views;
import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
import static com.android.launcher3.Utilities.boundToRange;
import static com.android.launcher3.Utilities.mapToRange;
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
@@ -97,6 +98,9 @@
* within the clip bounds of this view.
*/
public void setTaskViewArtist(TaskViewArtist taskViewArtist) {
+ if (!enableAdditionalHomeAnimations()) {
+ return;
+ }
mTaskViewArtist = taskViewArtist;
invalidate();
}
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 1d5a9dc..1e577be 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -18,6 +18,7 @@
import static android.view.Gravity.LEFT;
import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
import static com.android.launcher3.Utilities.getFullDrawable;
import static com.android.launcher3.Utilities.mapToRange;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -164,7 +165,12 @@
*/
public void update(float alpha, RectF rect, float progress, float shapeProgressStart,
float cornerRadius, boolean isOpening, int taskViewDrawAlpha) {
- setAlpha(isLaidOut() ? alpha : 0f);
+ // The non-running task home animation has some very funky first few frames because this
+ // FIV hasn't fully laid out. During those frames, hide this FIV and continue drawing the
+ // TaskView directly while transforming it in the place of this FIV. However, if we fade
+ // the TaskView at all, we need to display this FIV regardless.
+ setAlpha(!enableAdditionalHomeAnimations() || isLaidOut() || taskViewDrawAlpha < 255
+ ? alpha : 0f);
mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, isOpening, this,
mLauncher.getDeviceProfile(), taskViewDrawAlpha);
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index a5e22c5..1fb8c83 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -20,6 +20,7 @@
import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
import static com.android.launcher3.Flags.enableWorkspaceInflation;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo;
import android.appwidget.AppWidgetHost;
@@ -36,6 +37,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.BaseDraggingActivity;
@@ -44,6 +46,7 @@
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.ResourceBasedOverride;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.widget.LauncherAppWidgetHost.ListenableHostView;
@@ -51,6 +54,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntConsumer;
/**
@@ -77,7 +81,7 @@
protected final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
protected final List<ProviderChangedListener> mProviderChangedListeners = new ArrayList<>();
- protected int mFlags = FLAG_STATE_IS_NORMAL;
+ protected AtomicInteger mFlags = new AtomicInteger(FLAG_STATE_IS_NORMAL);
// TODO(b/191735836): Replace with ActivityOptions.KEY_SPLASH_SCREEN_STYLE when un-hidden
private static final String KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle";
@@ -96,6 +100,10 @@
context, appWidgetRemovedCallback, mProviderChangedListeners);
}
+ protected LooperExecutor getWidgetHolderExecutor() {
+ return UI_HELPER_EXECUTOR;
+ }
+
/**
* Starts listening to the widget updates from the server side
*/
@@ -104,21 +112,23 @@
return;
}
- try {
- mWidgetHost.startListening();
- } catch (Exception e) {
- if (!Utilities.isBinderSizeError(e)) {
- throw new RuntimeException(e);
+ getWidgetHolderExecutor().execute(() -> {
+ try {
+ mWidgetHost.startListening();
+ } catch (Exception e) {
+ if (!Utilities.isBinderSizeError(e)) {
+ throw new RuntimeException(e);
+ }
+ // We're willing to let this slide. The exception is being caused by the list of
+ // RemoteViews which is being passed back. The startListening relationship will
+ // have been established by this point, and we will end up populating the
+ // widgets upon bind anyway. See issue 14255011 for more context.
}
- // We're willing to let this slide. The exception is being caused by the list of
- // RemoteViews which is being passed back. The startListening relationship will
- // have been established by this point, and we will end up populating the
- // widgets upon bind anyway. See issue 14255011 for more context.
- }
- // TODO: Investigate why widgetHost.startListening() always return non-empty updates
- setListeningFlag(true);
+ // TODO: Investigate why widgetHost.startListening() always return non-empty updates
+ setListeningFlag(true);
- updateDeferredView();
+ MAIN_EXECUTOR.execute(() -> updateDeferredView());
+ });
}
/**
@@ -282,16 +292,23 @@
if (!WIDGETS_ENABLED) {
return;
}
- mWidgetHost.stopListening();
- setListeningFlag(false);
+ getWidgetHolderExecutor().execute(() -> {
+ mWidgetHost.stopListening();
+ setListeningFlag(false);
+ });
}
+ /**
+ * Update {@link FLAG_LISTENING} on {@link mFlags} after making binder calls from
+ * {@link sWidgetHost}.
+ */
+ @WorkerThread
protected void setListeningFlag(final boolean isListening) {
if (isListening) {
- mFlags |= FLAG_LISTENING;
+ mFlags.updateAndGet(old -> old | FLAG_LISTENING);
return;
}
- mFlags &= ~FLAG_LISTENING;
+ mFlags.updateAndGet(old -> old & ~FLAG_LISTENING);
}
/**
@@ -373,7 +390,7 @@
* as a result of using the same flow.
*/
protected LauncherAppWidgetHostView recycleExistingView(LauncherAppWidgetHostView view) {
- if ((mFlags & FLAG_LISTENING) == 0) {
+ if ((mFlags.get() & FLAG_LISTENING) == 0) {
if (view instanceof PendingAppWidgetHostView pv && pv.isDeferredWidget()) {
return view;
} else {
@@ -395,7 +412,7 @@
@NonNull
protected LauncherAppWidgetHostView createViewInternal(
int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
- if ((mFlags & FLAG_LISTENING) == 0) {
+ if ((mFlags.get() & FLAG_LISTENING) == 0) {
// Since the launcher hasn't started listening to widget updates, we can't simply call
// host.createView here because the later will make a binder call to retrieve
// RemoteViews from system process.
@@ -460,7 +477,7 @@
* @return True if the host is listening to the updates, false otherwise
*/
public boolean isListening() {
- return (mFlags & FLAG_LISTENING) != 0;
+ return (mFlags.get() & FLAG_LISTENING) != 0;
}
/**
@@ -469,16 +486,17 @@
*/
private void setShouldListenFlag(int flag, boolean on) {
if (on) {
- mFlags |= flag;
+ mFlags.updateAndGet(old -> old | flag);
} else {
- mFlags &= ~flag;
+ mFlags.updateAndGet(old -> old & ~flag);
}
final boolean listening = isListening();
- if (!listening && shouldListen(mFlags)) {
+ int currentFlag = mFlags.get();
+ if (!listening && shouldListen(currentFlag)) {
// Postpone starting listening until all flags are on.
startListening();
- } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) {
+ } else if (listening && (currentFlag & FLAG_ACTIVITY_STARTED) == 0) {
// Postpone stopping listening until the activity is stopped.
stopListening();
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.kt b/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.kt
new file mode 100644
index 0000000..c5f9f86
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3
+
+import com.android.launcher3.util.MainThreadInitializedObject
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxApplication
+import com.android.launcher3.util.SafeCloseable
+
+/**
+ * Initializes [MainThreadInitializedObject] instances for Robolectric tests.
+ *
+ * Unlike instrumentation tests, Robolectric creates a new application instance for each test, which
+ * could cause the various static objects defined in [MainThreadInitializedObject] to leak. Thus, a
+ * [SandboxApplication] for Robolectric tests can implement this interface to limit the lifecycle of
+ * these objects to a single test.
+ */
+interface RoboObjectInitializer {
+
+ /** Overrides an object with [type] to [value]. */
+ fun <T : SafeCloseable> initializeObject(type: MainThreadInitializedObject<T>, value: T)
+}
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 362596c..405dae7 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -159,13 +159,13 @@
mLauncher.getWorkspace().getWorkspaceIconsPositions();
assertThat(initialPositions.keySet()).containsAtLeastElementsIn(appNames);
- mLauncher.getWorkspace().getWorkspaceAppIcon(DUMMY_APP_NAME).uninstall();
- mLauncher.getWorkspace().verifyWorkspaceAppIconIsGone(
+ final Workspace workspace = mLauncher.getWorkspace().getWorkspaceAppIcon(
+ DUMMY_APP_NAME).uninstall();
+ workspace.verifyWorkspaceAppIconIsGone(
DUMMY_APP_NAME + " was expected to disappear after uninstall.", DUMMY_APP_NAME);
Log.d(UIOBJECT_STALE_ELEMENT, "second getWorkspaceIconsPositions()");
- Map<String, Point> finalPositions =
- mLauncher.getWorkspace().getWorkspaceIconsPositions();
+ Map<String, Point> finalPositions = workspace.getWorkspaceIconsPositions();
assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME);
} finally {
TestUtil.uninstallDummyApp();
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index 28a001f..d16674c 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -1,10 +1,13 @@
package com.android.launcher3.model
import android.appwidget.AppWidgetManager
+import android.content.Intent
import android.os.UserHandle
import android.platform.test.flag.junit.SetFlagsRule
+import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.launcher3.Flags
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
@@ -14,6 +17,7 @@
import com.android.launcher3.icons.cache.CachingLogic
import com.android.launcher3.icons.cache.IconCacheUpdateHandler
import com.android.launcher3.pm.UserCache
+import com.android.launcher3.provider.RestoreDbTask
import com.android.launcher3.ui.TestViewHelpers
import com.android.launcher3.util.Executors.MODEL_EXECUTOR
import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
@@ -21,21 +25,30 @@
import com.android.launcher3.util.UserIconInfo
import com.google.common.truth.Truth
import java.util.concurrent.CountDownLatch
+import junit.framework.Assert.assertEquals
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyList
+import org.mockito.ArgumentMatchers.anyMap
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
+import org.mockito.MockitoSession
import org.mockito.Spy
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
private const val INSERTION_STATEMENT_FILE = "databases/workspace_items.sql"
@@ -43,6 +56,20 @@
@RunWith(AndroidJUnit4::class)
class LoaderTaskTest {
private var context = SandboxModelContext()
+ private val expectedBroadcastModel =
+ FirstScreenBroadcastModel(
+ installerPackage = "installerPackage",
+ pendingCollectionItems = mutableSetOf("pendingCollectionItem"),
+ pendingWidgetItems = mutableSetOf("pendingWidgetItem"),
+ pendingHotseatItems = mutableSetOf("pendingHotseatItem"),
+ pendingWorkspaceItems = mutableSetOf("pendingWorkspaceItem"),
+ installedHotseatItems = mutableSetOf("installedHotseatItem"),
+ installedWorkspaceItems = mutableSetOf("installedWorkspaceItem"),
+ firstScreenInstalledWidgets = mutableSetOf("installedFirstScreenWidget"),
+ secondaryScreenInstalledWidgets = mutableSetOf("installedSecondaryScreenWidget")
+ )
+ private lateinit var mockitoSession: MockitoSession
+
@Mock private lateinit var app: LauncherAppState
@Mock private lateinit var bgAllAppsList: AllAppsList
@Mock private lateinit var modelDelegate: ModelDelegate
@@ -61,7 +88,11 @@
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
-
+ mockitoSession =
+ ExtendedMockito.mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .mockStatic(FirstScreenBroadcastHelper::class.java)
+ .startMocking()
val idp =
InvariantDeviceProfile().apply {
numRows = 5
@@ -90,6 +121,7 @@
@After
fun tearDown() {
context.onDestroy()
+ mockitoSession.finishMocking()
}
@Test
@@ -166,6 +198,141 @@
verify(bgAllAppsList, Mockito.never())
.setFlags(BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED, true)
}
+
+ @Test
+ fun `When launcher_broadcast_installed_apps and is restore then send installed item broadcast`() {
+ // Given
+ val spyContext = spy(context)
+ `when`(app.context).thenReturn(spyContext)
+ whenever(
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ anyOrNull(),
+ anyList(),
+ anyMap(),
+ anyList()
+ )
+ )
+ .thenReturn(listOf(expectedBroadcastModel))
+
+ whenever(
+ FirstScreenBroadcastHelper.sendBroadcastsForModels(
+ spyContext,
+ listOf(expectedBroadcastModel)
+ )
+ )
+ .thenCallRealMethod()
+
+ Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 1)
+ RestoreDbTask.setPending(spyContext)
+
+ // When
+ LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+ .runSyncOnBackgroundThread()
+
+ // Then
+ val argumentCaptor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(spyContext).sendBroadcast(argumentCaptor.capture())
+ val actualBroadcastIntent = argumentCaptor.value
+ assertEquals(expectedBroadcastModel.installerPackage, actualBroadcastIntent.`package`)
+ assertEquals(
+ ArrayList(expectedBroadcastModel.installedWorkspaceItems),
+ actualBroadcastIntent.getStringArrayListExtra("workspaceInstalledItems")
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.installedHotseatItems),
+ actualBroadcastIntent.getStringArrayListExtra("hotseatInstalledItems")
+ )
+ assertEquals(
+ ArrayList(
+ expectedBroadcastModel.firstScreenInstalledWidgets +
+ expectedBroadcastModel.secondaryScreenInstalledWidgets
+ ),
+ actualBroadcastIntent.getStringArrayListExtra("widgetInstalledItems")
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingCollectionItems),
+ actualBroadcastIntent.getStringArrayListExtra("folderItem")
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingWorkspaceItems),
+ actualBroadcastIntent.getStringArrayListExtra("workspaceItem")
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingHotseatItems),
+ actualBroadcastIntent.getStringArrayListExtra("hotseatItem")
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingWidgetItems),
+ actualBroadcastIntent.getStringArrayListExtra("widgetItem")
+ )
+ }
+
+ @Test
+ fun `When not a restore then installed item broadcast not sent`() {
+ // Given
+ val spyContext = spy(context)
+ `when`(app.context).thenReturn(spyContext)
+ whenever(
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ anyOrNull(),
+ anyList(),
+ anyMap(),
+ anyList()
+ )
+ )
+ .thenReturn(listOf(expectedBroadcastModel))
+
+ whenever(
+ FirstScreenBroadcastHelper.sendBroadcastsForModels(
+ spyContext,
+ listOf(expectedBroadcastModel)
+ )
+ )
+ .thenCallRealMethod()
+
+ Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 1)
+
+ // When
+ LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+ .runSyncOnBackgroundThread()
+
+ // Then
+ verify(spyContext, times(0)).sendBroadcast(any(Intent::class.java))
+ }
+
+ @Test
+ fun `When launcher_broadcast_installed_apps false then installed item broadcast not sent`() {
+ // Given
+ val spyContext = spy(context)
+ `when`(app.context).thenReturn(spyContext)
+ whenever(
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ anyOrNull(),
+ anyList(),
+ anyMap(),
+ anyList()
+ )
+ )
+ .thenReturn(listOf(expectedBroadcastModel))
+
+ whenever(
+ FirstScreenBroadcastHelper.sendBroadcastsForModels(
+ spyContext,
+ listOf(expectedBroadcastModel)
+ )
+ )
+ .thenCallRealMethod()
+
+ Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 0)
+ RestoreDbTask.setPending(spyContext)
+
+ // When
+ LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+ .runSyncOnBackgroundThread()
+
+ // Then
+ verify(spyContext, times(0)).sendBroadcast(any(Intent::class.java))
+ }
}
private fun LoaderTask.runSyncOnBackgroundThread() {