Merge "[Launcher3] Hides AllApps education view for trackpad" into main
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 98a2783..63412e9 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -348,4 +348,8 @@
<string name="bubble_bar_action_move_right">Move right</string>
<!-- Action in accessibility menu to dismiss all bubbles. [CHAR_LIMIT=30] -->
<string name="bubble_bar_action_dismiss_all">Dismiss all</string>
+ <!-- Accessibility announcement when the bubble bar expands. [CHAR LIMIT=NONE]-->
+ <string name="bubble_bar_accessibility_announce_expand">expand <xliff:g id="bubble_description" example="some title from Messages">%1$s</xliff:g></string>
+ <!-- Accessibility announcement when the bubble bar collapses. [CHAR LIMIT=NONE]-->
+ <string name="bubble_bar_accessibility_announce_collapse">collapse <xliff:g id="bubble_description" example="some title from Messages">%1$s</xliff:g></string>
</resources>
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 62cc0bb..6c7fe5b 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -19,10 +19,9 @@
import static com.android.launcher3.LauncherState.BACKGROUND_APP;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
import android.os.Debug;
-import android.os.SystemProperties;
import android.util.Log;
import android.view.View;
@@ -145,7 +144,7 @@
notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
}
- if (!enableDesktopWindowingWallpaperActivity() && wasVisible != isVisible) {
+ if (!WALLPAPER_ACTIVITY.isEnabled(mLauncher) && wasVisible != isVisible) {
// TODO: b/333533253 - Remove after flag rollout
if (mVisibleDesktopTasksCount > 0) {
setLauncherViewsVisibility(View.INVISIBLE);
@@ -189,7 +188,7 @@
notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
}
- if (enableDesktopWindowingWallpaperActivity()) {
+ if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
return;
}
// TODO: b/333533253 - Clean up after flag rollout
@@ -289,7 +288,7 @@
* TODO: b/333533253 - Remove after flag rollout
*/
private void setLauncherViewsVisibility(int visibility) {
- if (enableDesktopWindowingWallpaperActivity()) {
+ if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
return;
}
if (DEBUG) {
@@ -314,7 +313,7 @@
* TODO: b/333533253 - Remove after flag rollout
*/
private void markLauncherPaused() {
- if (enableDesktopWindowingWallpaperActivity()) {
+ if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
return;
}
if (DEBUG) {
@@ -331,7 +330,7 @@
* TODO: b/333533253 - Remove after flag rollout
*/
private void markLauncherResumed() {
- if (enableDesktopWindowingWallpaperActivity()) {
+ if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
return;
}
if (DEBUG) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 0add1c4..93a023d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -21,7 +21,7 @@
import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_VISIBLE;
import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IGNORE_IN_APP;
import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
import android.animation.Animator;
import android.animation.AnimatorSet;
@@ -214,7 +214,7 @@
DesktopVisibilityController desktopController =
LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
- if (!enableDesktopWindowingWallpaperActivity()
+ if (!WALLPAPER_ACTIVITY.isEnabled(mLauncher)
&& desktopController != null
&& desktopController.areDesktopTasksVisible()) {
// TODO: b/333533253 - Remove after flag rollout
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 3048243..800c594 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -315,6 +315,7 @@
new TaskbarTranslationController(this),
new TaskbarSpringOnStashController(this),
new TaskbarRecentAppsController(
+ this,
RecentsModel.INSTANCE.get(this),
LauncherActivityInterface.INSTANCE::getDesktopVisibilityController),
TaskbarEduTooltipController.newInstance(this),
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 5c08116..2cb950c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.taskbar
+import android.content.Context
import androidx.annotation.VisibleForTesting
import com.android.launcher3.Flags.enableRecentsInTaskbar
import com.android.launcher3.model.data.ItemInfo
@@ -26,8 +27,8 @@
import com.android.quickstep.RecentsModel
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
-import com.android.window.flags.Flags.enableDesktopWindowingMode
import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
+import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE
import java.io.PrintWriter
/**
@@ -36,6 +37,7 @@
* - When in Desktop Mode: show the currently running (open) Tasks
*/
class TaskbarRecentAppsController(
+ context: Context,
private val recentsModel: RecentsModel,
// Pass a provider here instead of the actual DesktopVisibilityController instance since that
// instance might not be available when this constructor is called.
@@ -44,7 +46,7 @@
// TODO(b/335401172): unify DesktopMode checks in Launcher.
var canShowRunningApps =
- enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps()
+ DESKTOP_WINDOWING_MODE.isEnabled(context) && enableDesktopWindowingTaskbarRunningApps()
@VisibleForTesting
set(isEnabledFromTest) {
field = isEnabledFromTest
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index fab7975..5d550ae 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -1228,6 +1228,7 @@
mWidthAnimator.reverse();
}
updateBubbleAccessibilityStates();
+ announceExpandedStateChange();
}
}
@@ -1344,6 +1345,26 @@
setContentDescription(contentDesc);
}
+ private void announceExpandedStateChange() {
+ final CharSequence selectedBubbleContentDesc;
+ if (mSelectedBubbleView != null) {
+ selectedBubbleContentDesc = mSelectedBubbleView.getContentDescription();
+ } else {
+ selectedBubbleContentDesc = getResources().getString(
+ R.string.bubble_bar_bubble_fallback_description);
+ }
+
+ final String msg;
+ if (mIsBarExpanded) {
+ msg = getResources().getString(R.string.bubble_bar_accessibility_announce_expand,
+ selectedBubbleContentDesc);
+ } else {
+ msg = getResources().getString(R.string.bubble_bar_accessibility_announce_collapse,
+ selectedBubbleContentDesc);
+ }
+ announceForAccessibility(msg);
+ }
+
private boolean isIconSizeOrPaddingUpdated(float newIconSize, float newBubbleBarPadding) {
return isIconSizeUpdated(newIconSize) || isPaddingUpdated(newBubbleBarPadding);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 74a673b..9270f68 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -73,6 +73,7 @@
private TaskbarInsetsController mTaskbarInsetsController;
private View.OnClickListener mBubbleClickListener;
private View.OnClickListener mBubbleBarClickListener;
+ private BubbleView.Controller mBubbleViewController;
// These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing
private final MultiValueAlpha mBubbleBarAlpha;
@@ -153,6 +154,31 @@
mBubbleBarController.updateBubbleBarLocation(location);
}
});
+
+ mBubbleViewController = new BubbleView.Controller() {
+ @Override
+ public BubbleBarLocation getBubbleBarLocation() {
+ return BubbleBarViewController.this.getBubbleBarLocation();
+ }
+
+ @Override
+ public void dismiss(BubbleView bubble) {
+ if (bubble.getBubble() != null) {
+ notifySysUiBubbleDismissed(bubble.getBubble());
+ }
+ onBubbleDismissed(bubble);
+ }
+
+ @Override
+ public void collapse() {
+ collapseBubbleBar();
+ }
+
+ @Override
+ public void updateBubbleBarLocation(BubbleBarLocation location) {
+ mBubbleBarController.updateBubbleBarLocation(location);
+ }
+ };
}
private void onBubbleClicked(BubbleView bubbleView) {
@@ -165,8 +191,7 @@
final String currentlySelected = mBubbleBarController.getSelectedBubbleKey();
if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) {
// Tapping the currently selected bubble while expanded collapses the view.
- setExpanded(false);
- mBubbleStashController.stashBubbleBar();
+ collapseBubbleBar();
} else {
mBubbleBarController.showAndSelectBubble(bubble);
}
@@ -196,6 +221,11 @@
}
}
+ private void collapseBubbleBar() {
+ setExpanded(false);
+ mBubbleStashController.stashBubbleBar();
+ }
+
/** Notifies that the stash state is changing. */
public void onStashStateChanging() {
if (isAnimatingNewBubble()) {
@@ -220,7 +250,7 @@
return mBubbleBarTranslationY;
}
- float getBubbleBarCollapsedHeight() {
+ public float getBubbleBarCollapsedHeight() {
return mBarView.getBubbleBarCollapsedHeight();
}
@@ -440,6 +470,7 @@
public void removeBubble(BubbleBarBubble b) {
if (b != null) {
mBarView.removeBubble(b.getView());
+ b.getView().setController(null);
} else {
Log.w(TAG, "removeBubble, bubble was null!");
}
@@ -450,6 +481,8 @@
BubbleBarBubble removedBubble, boolean isExpanding, boolean suppressAnimation) {
mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView());
addedBubble.getView().setOnClickListener(mBubbleClickListener);
+ addedBubble.getView().setController(mBubbleViewController);
+ removedBubble.getView().setController(null);
mBubbleDragController.setupBubbleView(addedBubble.getView());
if (!suppressAnimation) {
animateBubbleNotification(addedBubble, isExpanding, /* isUpdate= */ false);
@@ -464,6 +497,7 @@
mBarView.addBubble(b.getView());
b.getView().setOnClickListener(mBubbleClickListener);
mBubbleDragController.setupBubbleView(b.getView());
+ b.getView().setController(mBubbleViewController);
if (b instanceof BubbleBarOverflow) {
return;
@@ -580,8 +614,8 @@
mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen());
}
- /** Notifies {@link BubbleBarView} that the dragged bubble was dismissed. */
- public void onBubbleDragDismissed(BubbleView bubble) {
+ /** Handle given bubble being dismissed */
+ public void onBubbleDismissed(BubbleView bubble) {
mBubbleBarController.onBubbleDismissed(bubble);
mBarView.removeBubble(bubble);
}
@@ -624,10 +658,9 @@
}
/**
- * Called when given bubble was dismissed. Notifies SystemUI
- * @param bubble dismissed bubble item
+ * Notify SystemUI that the given bubble has been dismissed.
*/
- public void onDismissBubble(@NonNull BubbleBarItem bubble) {
+ public void notifySysUiBubbleDismissed(@NonNull BubbleBarItem bubble) {
mSystemUiProxy.dragBubbleToDismiss(bubble.getKey(), mTimeSource.currentTimeMillis());
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
index 6a63da8..5eebbd8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -143,7 +143,7 @@
if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
if (bubbleView.getBubble() != null) {
- mBubbleBarViewController.onDismissBubble(bubbleView.getBubble());
+ mBubbleBarViewController.notifySysUiBubbleDismissed(bubbleView.getBubble());
}
} else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
mBubbleBarViewController.onDismissAllBubbles();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index 8316b5b..656a266 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -153,7 +153,7 @@
@Override
protected void onDragDismiss() {
mBubblePinController.onDragEnd();
- mBubbleBarViewController.onBubbleDragDismissed(bubbleView);
+ mBubbleBarViewController.onBubbleDismissed(bubbleView);
mBubbleBarViewController.onBubbleDragEnd();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index acb6b4e..3bcaa16 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -23,11 +23,13 @@
import android.graphics.Outline;
import android.graphics.Path;
import android.graphics.Rect;
+import android.os.Bundle;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ImageView;
import androidx.constraintlayout.widget.ConstraintLayout;
@@ -36,6 +38,7 @@
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import com.android.wm.shell.common.bubbles.BubbleInfo;
// TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
@@ -73,6 +76,9 @@
private BubbleBarItem mBubble;
+ @Nullable
+ private Controller mController;
+
public BubbleView(Context context) {
this(context, null);
}
@@ -180,6 +186,58 @@
mDotRenderer.draw(canvas, mDrawParams);
}
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
+ if (mBubble instanceof BubbleBarBubble) {
+ info.addAction(AccessibilityNodeInfo.ACTION_DISMISS);
+ }
+ if (mController != null) {
+ if (mController.getBubbleBarLocation().isOnLeft(isLayoutRtl())) {
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right,
+ getResources().getString(R.string.bubble_bar_action_move_right)));
+ } else {
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left,
+ getResources().getString(R.string.bubble_bar_action_move_left)));
+ }
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
+ if (mController != null) {
+ mController.collapse();
+ }
+ return true;
+ }
+ if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
+ if (mController != null) {
+ mController.dismiss(this);
+ }
+ return true;
+ }
+ if (action == R.id.action_move_left) {
+ if (mController != null) {
+ mController.updateBubbleBarLocation(BubbleBarLocation.LEFT);
+ }
+ }
+ if (action == R.id.action_move_right) {
+ if (mController != null) {
+ mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT);
+ }
+ }
+ return false;
+ }
+
+ void setController(@Nullable Controller controller) {
+ mController = controller;
+ }
+
/** Sets the bubble being rendered in this view. */
public void setBubble(BubbleBarBubble bubble) {
mBubble = bubble;
@@ -337,4 +395,19 @@
String toString = mBubble != null ? mBubble.getKey() : "null";
return "BubbleView{" + toString + "}";
}
+
+ /** Interface for BubbleView to communicate with its controller */
+ public interface Controller {
+ /** Get current bubble bar {@link BubbleBarLocation} */
+ BubbleBarLocation getBubbleBarLocation();
+
+ /** This bubble should be dismissed */
+ void dismiss(BubbleView bubble);
+
+ /** Collapse the bubble bar */
+ void collapse();
+
+ /** Request bubble bar location to be updated to the given location */
+ void updateBubbleBarLocation(BubbleBarLocation location);
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
new file mode 100644
index 0000000..e42b6d6
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
@@ -0,0 +1,202 @@
+/*
+ * 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.stashing
+
+import android.view.InsetsController
+import android.view.MotionEvent
+import android.view.View
+import com.android.launcher3.taskbar.TaskbarInsetsController
+import com.android.launcher3.taskbar.bubbles.BubbleBarView
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+import java.io.PrintWriter
+
+/** StashController that defines stashing behaviour for the taskbar modes. */
+interface BubbleStashController {
+
+ /**
+ * Abstraction on the task bar activity context to only provide the dimensions required for
+ * [BubbleBarView] translation Y computation.
+ */
+ interface TaskbarHotseatDimensionsProvider {
+
+ /** Provides taskbar bottom space in pixels. */
+ fun getTaskbarBottomSpace(): Int
+
+ /** Provides taskbar height in pixels. */
+ fun getTaskbarHeight(): Int
+
+ /** Provides hotseat bottom space in pixels. */
+ fun getHotseatBottomSpace(): Int
+
+ /** Provides hotseat height in pixels. */
+ fun getHotseatHeight(): Int
+ }
+
+ /** Execute passed action only after controllers are initiated. */
+ interface ControllersAfterInitAction {
+ /** Execute action after controllers are initiated. */
+ fun runAfterInit(action: () -> Unit)
+ }
+
+ /** Whether bubble bar is currently stashed */
+ val isStashed: Boolean
+
+ /** Whether launcher enters or exits the home page. */
+ var isBubblesShowingOnHome: Boolean
+
+ /** Whether launcher enters or exits the overview page. */
+ var isBubblesShowingOnOverview: Boolean
+
+ /** Updated when sysui locked state changes, when locked, bubble bar is not shown. */
+ var isSysuiLocked: Boolean
+
+ /** Whether there is a transient taskbar mode */
+ val isTransientTaskBar: Boolean
+
+ /** Whether stash control has a handle view */
+ val hasHandleView: Boolean
+
+ /** Initialize controller */
+ fun init(
+ taskbarInsetsController: TaskbarInsetsController,
+ bubbleBarViewController: BubbleBarViewController,
+ bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
+ controllersAfterInitAction: ControllersAfterInitAction
+ )
+
+ /** Sets stashed and expanded state of the bubble bar */
+ fun updateStashedAndExpandedState(stash: Boolean = false, expand: Boolean = false)
+
+ /**
+ * Shows the bubble bar at [getBubbleBarTranslationY] position immediately without animation.
+ */
+ fun showBubbleBarImmediate()
+
+ /** Shows the bubble bar at [bubbleBarTranslationY] position immediately without animation. */
+ fun showBubbleBarImmediate(bubbleBarTranslationY: Float)
+
+ /** Stashes the bubble bar immediately without animation. */
+ fun stashBubbleBarImmediate()
+
+ /** Returns the touchable height of the bubble bar based on it's stashed state. */
+ fun getTouchableHeight(): Int
+
+ /** Whether bubble bar is currently visible */
+ fun isBubbleBarVisible(): Boolean
+
+ /**
+ * Updates the values of the internal animators after the new bubble animation was interrupted
+ *
+ * @param isStashed whether the current state should be stashed
+ * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the
+ * bubble bar is showing to ensure that the stash animator runs smoothly.
+ */
+ fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float)
+
+ /** Checks whether the motion event is over the stash handle or bubble bar. */
+ fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean
+
+ /** Set a bubble bar location */
+ fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation)
+
+ /**
+ * Stashes the bubble bar (transform to the handle view), or just shrink width of the expanded
+ * bubble bar based on the controller implementation.
+ */
+ fun stashBubbleBar() {
+ updateStashedAndExpandedState(stash = true, expand = false)
+ }
+
+ /** Shows the bubble bar, and expands bubbles depending on [expandBubbles]. */
+ fun showBubbleBar(expandBubbles: Boolean) {
+ updateStashedAndExpandedState(stash = false, expandBubbles)
+ }
+
+ // TODO(b/354218264): Move to BubbleBarViewAnimator
+ /**
+ * The difference on the Y axis between the center of the handle and the center of the bubble
+ * bar.
+ */
+ fun getSlideInAnimationDistanceY(): Float
+
+ // TODO(b/354218264): Move to BubbleBarViewAnimator
+ /** The distance the handle moves as part of the new bubble animation. */
+ fun getStashedHandleTranslationForNewBubbleAnimation(): Float
+
+ // TODO(b/354218264): Move to BubbleBarViewAnimator
+ /** Returns the [PhysicsAnimator] for the stashed handle view. */
+ fun getStashedHandlePhysicsAnimator(): PhysicsAnimator<View>?
+
+ // TODO(b/354218264): Move to BubbleBarViewAnimator
+ /** Notifies taskbar that it should update its touchable region. */
+ fun updateTaskbarTouchRegion()
+
+ // TODO(b/354218264): Move to BubbleBarViewAnimator
+ /** Set the translation Y for the stashed handle. */
+ fun setHandleTranslationY(translationY: Float)
+
+ /**
+ * Returns bubble bar Y position according to [isBubblesShowingOnHome] and
+ * [isBubblesShowingOnOverview] values. Default implementation only analyse
+ * [isBubblesShowingOnHome] and return translationY to align with the hotseat vertical center.
+ * For Other cases align bubbles with the taskbar.
+ */
+ val bubbleBarTranslationY: Float
+ get() =
+ if (isBubblesShowingOnHome) {
+ bubbleBarTranslationYForHotseat
+ } else {
+ bubbleBarTranslationYForTaskbar
+ }
+
+ /** Translation Y to align the bubble bar with the hotseat. */
+ val bubbleBarTranslationYForTaskbar: Float
+
+ /** Return translation Y to align the bubble bar with the taskbar. */
+ val bubbleBarTranslationYForHotseat: Float
+
+ /** Dumps the state of BubbleStashController. */
+ fun dump(pw: PrintWriter) {
+ pw.println("Bubble stash controller state:")
+ pw.println(" isStashed: $isStashed")
+ pw.println(" isBubblesShowingOnOverview: $isBubblesShowingOnOverview")
+ pw.println(" isBubblesShowingOnHome: $isBubblesShowingOnHome")
+ pw.println(" isSysuiLocked: $isSysuiLocked")
+ }
+
+ companion object {
+ /** How long to stash/unstash. */
+ const val BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE.toLong()
+
+ /** How long to translate Y coordinate of the BubbleBar. */
+ const val BAR_TRANSLATION_DURATION = 300L
+
+ /** The scale bubble bar animates to when being stashed. */
+ const val STASHED_BAR_SCALE = 0.5f
+
+ /** Creates new instance of [BubbleStashController] */
+ @JvmStatic
+ fun newInstance(
+ taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider
+ ): BubbleStashController {
+ return PersistentTaskbarStashController(taskbarHotseatDimensionsProvider)
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentTaskbarStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentTaskbarStashController.kt
new file mode 100644
index 0000000..4a05a5e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentTaskbarStashController.kt
@@ -0,0 +1,223 @@
+/*
+ * 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.stashing
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.view.MotionEvent
+import android.view.View
+import com.android.launcher3.anim.AnimatedFloat
+import com.android.launcher3.taskbar.TaskbarInsetsController
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+
+class PersistentTaskbarStashController(
+ private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider,
+) : BubbleStashController {
+
+ private lateinit var taskbarInsetsController: TaskbarInsetsController
+ private lateinit var bubbleBarViewController: BubbleBarViewController
+ private lateinit var bubbleBarTranslationYAnimator: AnimatedFloat
+ private lateinit var bubbleBarAlphaAnimator: MultiPropertyFactory<View>.MultiProperty
+ private lateinit var bubbleBarScaleAnimator: AnimatedFloat
+ private lateinit var controllersAfterInitAction: ControllersAfterInitAction
+
+ override var isBubblesShowingOnHome: Boolean = false
+ set(onHome) {
+ if (field == onHome) return
+ field = onHome
+ if (!bubbleBarViewController.hasBubbles()) {
+ // if there are no bubbles, there's nothing to show, so just return.
+ return
+ }
+ if (onHome) {
+ // When transition to home we should show collapse the bubble bar
+ updateStashedAndExpandedState(stash = false, expand = false)
+ }
+ animateBubbleBarY()
+ bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
+ }
+
+ override var isBubblesShowingOnOverview: Boolean = false
+ set(onOverview) {
+ if (field == onOverview) return
+ field = onOverview
+ if (!onOverview) {
+ // When transition from overview we should show collapse the bubble bar
+ updateStashedAndExpandedState(stash = false, expand = false)
+ }
+ bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
+ }
+
+ override var isSysuiLocked: Boolean = false
+ set(isLocked) {
+ if (field == isLocked) return
+ field = isLocked
+ if (!isLocked && bubbleBarViewController.hasBubbles()) {
+ animateAfterUnlock()
+ }
+ }
+
+ override var isTransientTaskBar: Boolean = false
+
+ /** When the bubble bar is shown for the persistent task bar, there is no handle view. */
+ override val hasHandleView: Boolean = false
+
+ /** For persistent task bar we never stash the bubble bar */
+ override val isStashed: Boolean = false
+
+ override val bubbleBarTranslationYForTaskbar: Float
+ get() {
+ val taskbarBottomMargin = taskbarHotseatDimensionsProvider.getTaskbarBottomSpace()
+ val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight
+ val taskbarHeight = taskbarHotseatDimensionsProvider.getTaskbarHeight()
+ return -taskbarBottomMargin - (taskbarHeight - bubbleBarHeight) / 2f
+ }
+
+ override val bubbleBarTranslationYForHotseat: Float
+ get() {
+ val hotseatBottomSpace = taskbarHotseatDimensionsProvider.getHotseatBottomSpace()
+ val hotseatCellHeight = taskbarHotseatDimensionsProvider.getHotseatHeight()
+ val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight
+ return -hotseatBottomSpace - (hotseatCellHeight - bubbleBarHeight) / 2
+ }
+
+ override fun init(
+ taskbarInsetsController: TaskbarInsetsController,
+ bubbleBarViewController: BubbleBarViewController,
+ bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
+ controllersAfterInitAction: ControllersAfterInitAction
+ ) {
+ this.taskbarInsetsController = taskbarInsetsController
+ this.bubbleBarViewController = bubbleBarViewController
+ this.controllersAfterInitAction = controllersAfterInitAction
+ bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY
+ bubbleBarAlphaAnimator = bubbleBarViewController.bubbleBarAlpha.get(0)
+ bubbleBarScaleAnimator = bubbleBarViewController.bubbleBarScale
+ }
+
+ private fun animateAfterUnlock() {
+ val animatorSet = AnimatorSet()
+ if (isBubblesShowingOnHome || isBubblesShowingOnOverview) {
+ animatorSet.playTogether(
+ bubbleBarScaleAnimator.animateToValue(1f),
+ bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY),
+ bubbleBarAlphaAnimator.animateToValue(1f)
+ )
+ }
+ updateTouchRegionOnAnimationEnd(animatorSet)
+ animatorSet.setDuration(BAR_STASH_DURATION).start()
+ }
+
+ override fun updateStashedAndExpandedState(stash: Boolean, expand: Boolean) {
+ if (bubbleBarViewController.isHiddenForNoBubbles) {
+ // If there are no bubbles the bar is invisible, nothing to do here.
+ return
+ }
+ if (bubbleBarViewController.isExpanded != expand) {
+ bubbleBarViewController.isExpanded = expand
+ }
+ }
+
+ override fun showBubbleBarImmediate() = showBubbleBarImmediate(bubbleBarTranslationY)
+
+ override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) {
+ bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY)
+ bubbleBarAlphaAnimator.setValue(1f)
+ bubbleBarScaleAnimator.updateValue(1f)
+ }
+
+ override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) {
+ // When the bubble bar is shown for the persistent task bar, there is no handle view, so no
+ // operation is performed.
+ }
+
+ override fun stashBubbleBarImmediate() {
+ // When the bubble bar is shown for the persistent task bar, there is no handle view, so no
+ // operation is performed.
+ }
+
+ /** If bubble bar is visible return bubble bar height, 0 otherwise */
+ override fun getTouchableHeight() =
+ if (isBubbleBarVisible()) {
+ bubbleBarViewController.bubbleBarCollapsedHeight.toInt()
+ } else {
+ 0
+ }
+
+ override fun isBubbleBarVisible(): Boolean = bubbleBarViewController.hasBubbles()
+
+ override fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) {
+ showBubbleBarImmediate(bubbleBarTranslationY)
+ }
+
+ override fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean =
+ bubbleBarViewController.isEventOverAnyItem(ev)
+
+ override fun getSlideInAnimationDistanceY(): Float {
+ // distance from the bottom of the screen and the bubble bar center.
+ return -bubbleBarViewController.bubbleBarCollapsedHeight / 2f
+ }
+
+ /** When the bubble bar is shown for the persistent task bar, there is no handle view. */
+ override fun getStashedHandleTranslationForNewBubbleAnimation(): Float = 0f
+
+ /** When the bubble bar is shown for the persistent task bar, there is no handle view. */
+ override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator<View>? = null
+
+ override fun updateTaskbarTouchRegion() {
+ taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+ }
+
+ /**
+ * When the bubble bar is shown for the persistent task bar the bar does not stash, so no
+ * operation is performed
+ */
+ override fun setHandleTranslationY(translationY: Float) {
+ // no op since does not have a handle view
+ }
+
+ /** Animates bubble bar Y accordingly to the showing mode */
+ private fun animateBubbleBarY() {
+ val animator =
+ bubbleBarViewController.bubbleBarTranslationY.animateToValue(bubbleBarTranslationY)
+ updateTouchRegionOnAnimationEnd(animator)
+ animator.setDuration(BAR_TRANSLATION_DURATION)
+ animator.start()
+ }
+
+ private fun updateTouchRegionOnAnimationEnd(animator: Animator) {
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+
+ override fun onAnimationEnd(animation: Animator) {
+ controllersAfterInitAction.runAfterInit {
+ taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 57f6934..5176d74 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -64,10 +64,10 @@
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.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -201,6 +201,8 @@
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;
@@ -213,8 +215,6 @@
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);
@@ -278,7 +278,7 @@
// TODO(b/337863494): Explore use of the same OverviewComponentObserver across launcher
OverviewComponentObserver overviewComponentObserver = new OverviewComponentObserver(
asContext(), deviceState);
- if (enableDesktopWindowingMode()) {
+ if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
getStateManager(), systemUiProxy, getIApplicationThread(),
getDepthController());
@@ -298,7 +298,7 @@
mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
mDepthController = new DepthController(this);
- if (enableDesktopWindowingMode()) {
+ if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
mDesktopVisibilityController = new DesktopVisibilityController(this);
mDesktopVisibilityController.registerSystemUiListener();
mSplitSelectStateController.initSplitFromDesktopController(this,
@@ -1006,7 +1006,7 @@
@Override
public void setResumed() {
- if (!enableDesktopWindowingWallpaperActivity()
+ if (!WALLPAPER_ACTIVITY.isEnabled(this)
&& mDesktopVisibilityController != null
&& mDesktopVisibilityController.areDesktopTasksVisible()
&& !mDesktopVisibilityController.isRecentsGestureInProgress()) {
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 20eaddc..b059186 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -62,6 +62,8 @@
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -152,6 +154,8 @@
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils;
+import kotlin.Unit;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -161,8 +165,6 @@
import java.util.OptionalInt;
import java.util.function.Consumer;
-import kotlin.Unit;
-
/**
* Handles the navigation gestures when Launcher is the default home activity.
*/
@@ -952,7 +954,7 @@
public void onRecentsAnimationStart(RecentsAnimationController controller,
RecentsAnimationTargets targets) {
super.onRecentsAnimationStart(controller, targets);
- if (targets.hasDesktopTasks()) {
+ if (targets.hasDesktopTasks(mContext)) {
mRemoteTargetHandles = mTargetGluer.assignTargetsForDesktop(targets);
} else {
int untrimmedAppCount = mRemoteTargetHandles.length;
@@ -1272,8 +1274,8 @@
TaskView currentPageTaskView = mRecentsView != null
? mRecentsView.getCurrentPageTaskView() : null;
- if (Flags.enableDesktopWindowingMode()
- && !(Flags.enableDesktopWindowingWallpaperActivity()
+ if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+ && !(WALLPAPER_ACTIVITY.isEnabled(mContext)
&& Flags.enableDesktopWindowingQuickSwitch())) {
if ((nextPageTaskView instanceof DesktopTaskView
|| currentPageTaskView instanceof DesktopTaskView)
@@ -1445,8 +1447,8 @@
setClampScrollOffset(false);
};
- if (Flags.enableDesktopWindowingMode()
- && !(Flags.enableDesktopWindowingWallpaperActivity()
+ if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+ && !(WALLPAPER_ACTIVITY.isEnabled(mContext)
&& Flags.enableDesktopWindowingQuickSwitch())) {
if (mRecentsView != null && (mRecentsView.getCurrentPageTaskView() != null
&& !(mRecentsView.getCurrentPageTaskView() instanceof DesktopTaskView))) {
@@ -2293,8 +2295,8 @@
mRecentsAnimationController, mRecentsAnimationTargets);
});
- if (Flags.enableDesktopWindowingMode()
- && !(Flags.enableDesktopWindowingWallpaperActivity()
+ if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+ && !(WALLPAPER_ACTIVITY.isEnabled(mContext)
&& Flags.enableDesktopWindowingQuickSwitch())) {
if (mRecentsView.getNextPageTaskView() instanceof DesktopTaskView
|| mRecentsView.getCurrentPageTaskView() instanceof DesktopTaskView) {
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 4989831..95c86fa 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -20,13 +20,14 @@
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM;
import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.app.TaskInfo;
import android.content.ComponentName;
+import android.content.Context;
import android.os.Process;
import android.os.RemoteException;
import android.util.SparseBooleanArray;
@@ -58,6 +59,7 @@
private static final TaskLoadResult INVALID_RESULT = new TaskLoadResult(-1, false, 0);
+ private final Context mContext;
private final KeyguardManager mKeyguardManager;
private final LooperExecutor mMainThreadExecutor;
private final SystemUiProxy mSysUiProxy;
@@ -76,8 +78,10 @@
// Tasks are stored in order of least recently launched to most recently launched.
private ArrayList<ActivityManager.RunningTaskInfo> mRunningTasks;
- public RecentTasksList(LooperExecutor mainThreadExecutor, KeyguardManager keyguardManager,
- SystemUiProxy sysUiProxy, TopTaskTracker topTaskTracker) {
+ public RecentTasksList(Context context, LooperExecutor mainThreadExecutor,
+ KeyguardManager keyguardManager, SystemUiProxy sysUiProxy,
+ TopTaskTracker topTaskTracker) {
+ mContext = context;
mMainThreadExecutor = mainThreadExecutor;
mKeyguardManager = keyguardManager;
mChangeId = 1;
@@ -325,9 +329,9 @@
int numVisibleTasks = 0;
for (GroupedRecentTaskInfo rawTask : rawTasks) {
if (rawTask.getType() == TYPE_FREEFORM) {
- // TYPE_FREEFORM tasks is only created when enableDesktopWindowingMode() is true,
+ // TYPE_FREEFORM tasks is only created whenDESKTOP_WINDOWING_MODE.isEnabled is true,
// leftover TYPE_FREEFORM tasks created when flag was on should be ignored.
- if (enableDesktopWindowingMode()) {
+ if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)) {
GroupTask desktopTask = createDesktopTask(rawTask);
if (desktopTask != null) {
allTasks.add(desktopTask);
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 18461a6..e84200d 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -27,7 +27,7 @@
import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely;
import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -147,7 +147,7 @@
mActionsView = findViewById(R.id.overview_actions_view);
getRootView().getSysUiScrim().getSysUIProgress().updateValue(0);
mDragLayer.recreateControllers();
- if (enableDesktopWindowingMode()) {
+ if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
getStateManager(), systemUiProxy, getIApplicationThread(),
null /* depthController */
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
index 82bb453..d104911 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -18,9 +18,10 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
import android.app.WindowConfiguration;
+import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.RemoteAnimationTarget;
@@ -54,8 +55,8 @@
*
* @return {@code true} if at least one target app is a desktop task
*/
- public boolean hasDesktopTasks() {
- if (!enableDesktopWindowingMode()) {
+ public boolean hasDesktopTasks(Context context) {
+ if (!DESKTOP_WINDOWING_MODE.isEnabled(context)) {
return false;
}
for (RemoteAnimationTarget target : apps) {
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index f2b6005..db03dac 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -90,7 +90,9 @@
private RecentsModel(Context context, IconProvider iconProvider) {
this(context,
- new RecentTasksList(MAIN_EXECUTOR,
+ new RecentTasksList(
+ context,
+ MAIN_EXECUTOR,
context.getSystemService(KeyguardManager.class),
SystemUiProxy.INSTANCE.get(context),
TopTaskTracker.INSTANCE.get(context)),
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 3f73959..f2db5af 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -23,8 +23,8 @@
import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING;
import static com.android.quickstep.util.LogUtils.splitFailureMessage;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -1444,7 +1444,8 @@
private boolean shouldEnableRunningTasksForDesktopMode() {
// TODO(b/335401172): unify DesktopMode checks in Launcher
- return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps();
+ return DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+ && enableDesktopWindowingTaskbarRunningApps();
}
private boolean handleMessageAsync(Message msg) {
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index 8e3d44f..31aca03 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -17,18 +17,28 @@
package com.android.quickstep.util;
import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import android.animation.AnimatorSet;
+import android.annotation.NonNull;
import android.os.Bundle;
import android.os.IRemoteCallback;
import android.view.animation.Interpolator;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.util.RunnableList;
+import com.android.quickstep.views.RecentsViewContainer;
/**
* Utility class containing methods to help manage animations, interpolators, and timings.
*/
public class AnimUtils {
+ private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
+
/**
* Fetches device-specific timings for the Overview > Split animation
* (splitscreen initiated from Overview).
@@ -59,6 +69,33 @@
}
/**
+ * Synchronizes the timing for the split dismiss animation to the current transition to
+ * NORMAL (launcher home/workspace)
+ */
+ public static void goToNormalStateWithSplitDismissal(@NonNull StateManager stateManager,
+ @NonNull RecentsViewContainer container,
+ @NonNull StatsLogManager.LauncherEvent exitReason,
+ @NonNull SplitAnimationController animationController) {
+ StateAnimationConfig config = new StateAnimationConfig();
+ BaseState startState = stateManager.getState();
+ long duration = startState.getTransitionDuration(container.asContext(),
+ false /*isToState*/);
+ if (duration == 0) {
+ // Case where we're in contextual on workspace (NORMAL), which by default has 0
+ // transition duration
+ duration = DURATION_DEFAULT_SPLIT_DISMISS;
+ }
+ config.duration = duration;
+ AnimatorSet stateAnim = stateManager.createAtomicAnimation(
+ startState, NORMAL, config);
+ AnimatorSet dismissAnim = animationController
+ .createPlaceholderDismissAnim(container, exitReason, duration);
+ stateAnim.play(dismissAnim);
+ stateManager.setCurrentAnimation(stateAnim, NORMAL);
+ stateAnim.start();
+ }
+
+ /**
* Returns a IRemoteCallback which completes the provided list as a result
*/
public static IRemoteCallback completeRunnableListCallback(RunnableList list) {
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index e48a7c6..d20d0a5 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -26,7 +26,7 @@
import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
import static com.android.launcher3.LauncherState.SPRING_LOADED;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
import android.annotation.TargetApi;
import android.content.Context;
@@ -54,6 +54,7 @@
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.RotationTouchHelper;
import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AnimUtils;
import com.android.quickstep.util.SplitSelectStateController;
import com.android.systemui.shared.recents.model.Task;
@@ -91,10 +92,12 @@
protected void handleStartHome(boolean animated) {
StateManager stateManager = getStateManager();
animated &= stateManager.shouldAnimateStateChange();
- stateManager.goToState(NORMAL, animated);
- if (FeatureFlags.enableSplitContextually()) {
- mSplitSelectStateController.getSplitAnimationController()
- .playPlaceholderDismissAnim(mContainer, LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
+ if (mSplitSelectStateController.isSplitSelectActive()) {
+ AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+ LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
+ mSplitSelectStateController.getSplitAnimationController());
+ } else {
+ stateManager.goToState(NORMAL, animated);
}
AbstractFloatingView.closeAllOpenViews(mContainer, animated);
}
@@ -265,7 +268,7 @@
super.onGestureAnimationStart(runningTasks, rotationTouchHelper);
DesktopVisibilityController desktopVisibilityController =
mContainer.getDesktopVisibilityController();
- if (!enableDesktopWindowingWallpaperActivity() && desktopVisibilityController != null) {
+ if (!WALLPAPER_ACTIVITY.isEnabled(mContext) && desktopVisibilityController != null) {
// TODO: b/333533253 - Remove after flag rollout
desktopVisibilityController.setRecentsGestureStart();
}
@@ -288,7 +291,7 @@
}
}
super.onGestureAnimationEnd();
- if (!enableDesktopWindowingWallpaperActivity() && desktopVisibilityController != null) {
+ if (!WALLPAPER_ACTIVITY.isEnabled(mContext) && desktopVisibilityController != null) {
// TODO: b/333533253 - Remove after flag rollout
desktopVisibilityController.setRecentsGestureEnd(endTarget);
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index a3d6359..d63ac56 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -5498,7 +5498,7 @@
}
RemoteTargetGluer gluer;
- if (recentsAnimationTargets.hasDesktopTasks()) {
+ if (recentsAnimationTargets.hasDesktopTasks(mContext)) {
gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
true /* forDesktop */);
mRemoteTargetHandles = gluer.assignTargetsForDesktop(recentsAnimationTargets);
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index 3d994e8..f6393e4 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -16,13 +16,11 @@
package com.android.quickstep.views;
-import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON;
import static com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
@@ -42,9 +40,8 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.util.AnimUtils;
import com.android.quickstep.util.SplitSelectStateController;
/**
@@ -57,7 +54,6 @@
public class SplitInstructionsView extends LinearLayout {
private static final int BOUNCE_DURATION = 250;
private static final float BOUNCE_HEIGHT = 20;
- private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
private final RecentsViewContainer mContainer;
public boolean mIsCurrentlyAnimating = false;
@@ -165,25 +161,11 @@
private void exitSplitSelection() {
RecentsView recentsView = mContainer.getOverviewPanel();
SplitSelectStateController splitSelectController = recentsView.getSplitSelectController();
-
StateManager stateManager = recentsView.getStateManager();
- BaseState startState = stateManager.getState();
- long duration = startState.getTransitionDuration(mContainer.asContext(), false);
- if (duration == 0) {
- // Case where we're in contextual on workspace (NORMAL), which by default has 0
- // transition duration
- duration = DURATION_DEFAULT_SPLIT_DISMISS;
- }
- StateAnimationConfig config = new StateAnimationConfig();
- config.duration = duration;
- AnimatorSet stateAnim = stateManager.createAtomicAnimation(
- startState, NORMAL, config);
- AnimatorSet dismissAnim = splitSelectController.getSplitAnimationController()
- .createPlaceholderDismissAnim(mContainer,
- LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, duration);
- stateAnim.play(dismissAnim);
- stateManager.setCurrentAnimation(stateAnim, NORMAL);
- stateAnim.start();
+
+ AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+ LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON,
+ splitSelectController.getSplitAnimationController());
}
void ensureProperRotation() {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentTaskbarStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentTaskbarStashControllerTest.kt
new file mode 100644
index 0000000..c46c08d
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentTaskbarStashControllerTest.kt
@@ -0,0 +1,256 @@
+/*
+ * 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.stashing
+
+import android.animation.AnimatorTestRule
+import android.content.Context
+import android.widget.FrameLayout
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.anim.AnimatedFloat
+import com.android.launcher3.taskbar.TaskbarInsetsController
+import com.android.launcher3.taskbar.bubbles.BubbleBarView
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.util.MultiValueAlpha
+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.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** Unit tests for [PersistentTaskbarStashController]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PersistentTaskbarStashControllerTest {
+
+ companion object {
+ const val BUBBLE_BAR_HEIGHT = 100f
+ const val HOTSEAT_TRANSLATION_Y = -45f
+ const val TASK_BAR_TRANSLATION_Y = -5f
+ }
+
+ @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
+ @get:Rule val rule: MockitoRule = MockitoJUnit.rule()
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private lateinit var bubbleBarView: BubbleBarView
+
+ @Mock lateinit var bubbleBarViewController: BubbleBarViewController
+ @Mock lateinit var taskbarInsetsController: TaskbarInsetsController
+
+ private lateinit var persistentTaskBarStashController: PersistentTaskbarStashController
+ private lateinit var translationY: AnimatedFloat
+ private lateinit var scale: AnimatedFloat
+ private lateinit var alpha: MultiValueAlpha
+
+ @Before
+ fun setUp() {
+ persistentTaskBarStashController =
+ PersistentTaskbarStashController(DefaultDimensionsProvider())
+ setUpBubbleBarView()
+ setUpBubbleBarController()
+ persistentTaskBarStashController.init(
+ taskbarInsetsController,
+ bubbleBarViewController,
+ null,
+ ImmediateAction()
+ )
+ }
+
+ @Test
+ fun setBubblesShowingOnHomeUpdatedToFalse_barPositionYUpdated_controllersNotified() {
+ // Given bubble bar is on home and has bubbles
+ whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+ persistentTaskBarStashController.isBubblesShowingOnHome = true
+ whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+ // When switch out of the home screen
+ getInstrumentation().runOnMainSync {
+ persistentTaskBarStashController.isBubblesShowingOnHome = false
+ }
+
+ // Then translation Y is animating and the bubble bar controller is notified
+ assertThat(translationY.isAnimating).isTrue()
+ verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+ // Wait until animation ends
+ advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+ // Check translation Y is correct and the insets controller is notified
+ assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y)
+ verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+ }
+
+ @Test
+ fun setBubblesShowingOnHomeUpdatedToTrue_barPositionYUpdated_controllersNotified() {
+ // Given bubble bar has bubbles
+ whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+ // When switch to home screen
+ getInstrumentation().runOnMainSync {
+ persistentTaskBarStashController.isBubblesShowingOnHome = true
+ }
+
+ // Then translation Y is animating and the bubble bar controller is notified
+ assertThat(translationY.isAnimating).isTrue()
+ verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+ // Wait until animation ends
+ advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+
+ // Check translation Y is correct and the insets controller is notified
+ assertThat(bubbleBarView.translationY).isEqualTo(HOTSEAT_TRANSLATION_Y)
+ verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+ }
+
+ @Test
+ fun setBubblesShowingOnOverviewUpdatedToFalse_controllersNotified() {
+ // Given bubble bar is on overview
+ persistentTaskBarStashController.isBubblesShowingOnOverview = true
+ clearInvocations(bubbleBarViewController)
+
+ // When switch out of the overview screen
+ persistentTaskBarStashController.isBubblesShowingOnOverview = false
+
+ // Then bubble bar controller is notified
+ verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+ }
+
+ @Test
+ fun setBubblesShowingOnOverviewUpdatedToTrue_controllersNotified() {
+ // When switch to the overview screen
+ persistentTaskBarStashController.isBubblesShowingOnOverview = true
+
+ // Then bubble bar controller is notified
+ verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+ }
+
+ @Test
+ fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() {
+ // Given screen is locked and bubble bar has bubbles
+ persistentTaskBarStashController.isSysuiLocked = true
+ persistentTaskBarStashController.isBubblesShowingOnOverview = true
+ whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+ // When switch to the overview screen
+ getInstrumentation().runOnMainSync {
+ persistentTaskBarStashController.isSysuiLocked = false
+ }
+
+ // Then
+ assertThat(translationY.isAnimating).isTrue()
+ assertThat(scale.isAnimating).isTrue()
+ // Wait until animation ends
+ advanceTimeBy(BubbleStashController.BAR_STASH_DURATION)
+
+ // Then bubble bar is fully visible at the correct location
+ assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+ assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+ assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y)
+ assertThat(bubbleBarView.alpha).isEqualTo(1f)
+ // Insets controller is notified
+ verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+ }
+
+ @Test
+ fun showBubbleBarImmediateToY() {
+ // Given bubble bar is fully transparent and scaled to 0 at 0 y position
+ val targetY = 341f
+ bubbleBarView.alpha = 0f
+ bubbleBarView.scaleX = 0f
+ bubbleBarView.scaleY = 0f
+ bubbleBarView.translationY = 0f
+
+ // When
+ persistentTaskBarStashController.showBubbleBarImmediate(targetY)
+
+ // Then all property values are updated
+ assertThat(bubbleBarView.translationY).isEqualTo(targetY)
+ assertThat(bubbleBarView.alpha).isEqualTo(1f)
+ assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+ assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+ }
+
+ @Test
+ fun isTransientTaskbar_false() {
+ assertThat(persistentTaskBarStashController.isTransientTaskBar).isFalse()
+ }
+
+ @Test
+ fun hasHandleView_false() {
+ assertThat(persistentTaskBarStashController.hasHandleView).isFalse()
+ }
+
+ @Test
+ fun isStashed_false() {
+ assertThat(persistentTaskBarStashController.isStashed).isFalse()
+ }
+
+ @Test
+ fun bubbleBarTranslationYForTaskbar() {
+ // Give bubble bar is on home
+ whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+ persistentTaskBarStashController.isBubblesShowingOnHome = true
+
+ // Then bubbleBarTranslationY would be HOTSEAT_TRANSLATION_Y
+ assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+ .isEqualTo(HOTSEAT_TRANSLATION_Y)
+
+ // Give bubble bar is not on home
+ persistentTaskBarStashController.isBubblesShowingOnHome = false
+
+ // Then bubbleBarTranslationY would be TASK_BAR_TRANSLATION_Y
+ assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+ .isEqualTo(TASK_BAR_TRANSLATION_Y)
+ }
+
+ private fun advanceTimeBy(advanceMs: Long) {
+ // Advance animator for on-device tests
+ getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(advanceMs) }
+ }
+
+ private fun setUpBubbleBarView() {
+ getInstrumentation().runOnMainSync {
+ bubbleBarView = BubbleBarView(context)
+ bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
+ }
+ }
+
+ private fun setUpBubbleBarController() {
+ translationY = AnimatedFloat(Runnable { bubbleBarView.translationY = translationY.value })
+ scale =
+ AnimatedFloat(
+ Runnable {
+ val scale: Float = scale.value
+ bubbleBarView.scaleX = scale
+ bubbleBarView.scaleY = scale
+ }
+ )
+ alpha = MultiValueAlpha(bubbleBarView, 1 /* num alpha channels */)
+
+ whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+ whenever(bubbleBarViewController.bubbleBarTranslationY).thenReturn(translationY)
+ whenever(bubbleBarViewController.bubbleBarScale).thenReturn(scale)
+ whenever(bubbleBarViewController.bubbleBarAlpha).thenReturn(alpha)
+ whenever(bubbleBarViewController.bubbleBarCollapsedHeight).thenReturn(BUBBLE_BAR_HEIGHT)
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
new file mode 100644
index 0000000..5dc9440
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.stashing
+
+class ImmediateAction : BubbleStashController.ControllersAfterInitAction {
+ override fun runAfterInit(action: () -> Unit) = action.invoke()
+}
+
+class DefaultDimensionsProvider : BubbleStashController.TaskbarHotseatDimensionsProvider {
+ override fun getTaskbarBottomSpace(): Int = TASKBAR_BOTTOM_SPACE
+
+ override fun getTaskbarHeight(): Int = TASKBAR_HEIGHT
+
+ override fun getHotseatBottomSpace(): Int = HOTSEAT_BOTTOM_SPACE
+
+ override fun getHotseatHeight(): Int = HOTSEAT_HEIGHT
+
+ companion object {
+ const val TASKBAR_BOTTOM_SPACE = 0
+ const val TASKBAR_HEIGHT = 110
+ const val HOTSEAT_BOTTOM_SPACE = 20
+ const val HOTSEAT_HEIGHT = 150
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
index 070eeaf..ea2e484 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -20,9 +20,18 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.THEMED_ICONS
import com.android.launcher3.LauncherPrefs.Companion.backedUpItem
import com.android.launcher3.logging.InstanceId
import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_ROTATION_DISABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_ROTATION_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -52,18 +61,24 @@
@Captor private lateinit var mEventCaptor: ArgumentCaptor<StatsLogManager.EventEnum>
+ private var mDefaultThemedIcons = false
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(mStatsLogManager.logger()).doReturn(mMockLogger)
whenever(mStatsLogManager.logger().withInstanceId(any())).doReturn(mMockLogger)
+ mDefaultThemedIcons = LauncherPrefs.get(mContext).get(THEMED_ICONS)
+ // To match the default value of THEMED_ICONS
+ LauncherPrefs.get(mContext).put(THEMED_ICONS, false)
mSystemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
}
@After
fun tearDown() {
+ LauncherPrefs.get(mContext).put(THEMED_ICONS, mDefaultThemedIcons)
mSystemUnderTest.close()
}
@@ -87,7 +102,8 @@
assertThat(capturedEvents.isNotEmpty()).isTrue()
verifyDefaultEvent(capturedEvents)
// pref_allowRotation false
- assertThat(capturedEvents.any { it.id == 616 }).isTrue()
+ assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_ROTATION_DISABLED.id })
+ .isTrue()
}
@Test
@@ -108,24 +124,22 @@
val capturedEvents = mEventCaptor.allValues
assertThat(capturedEvents.isNotEmpty()).isTrue()
verifyDefaultEvent(capturedEvents)
- // pref_allowRotation true
- assertThat(capturedEvents.any { it.id == 615 }).isTrue()
+ assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_ROTATION_ENABLED.id })
+ .isTrue()
}
private fun verifyDefaultEvent(capturedEvents: MutableList<StatsLogManager.EventEnum>) {
- // LAUNCHER_NOTIFICATION_DOT_ENABLED
- assertThat(capturedEvents.any { it.id == 611 }).isTrue()
- // LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON
- assertThat(capturedEvents.any { it.id == 625 }).isTrue()
- // LAUNCHER_THEMED_ICON_DISABLED
- assertThat(capturedEvents.any { it.id == 837 }).isTrue()
- // pref_add_icon_to_home true
- assertThat(capturedEvents.any { it.id == 613 }).isTrue()
- // pref_overview_action_suggestions true
- assertThat(capturedEvents.any { it.id == 619 }).isTrue()
- // pref_smartspace_home_screen true
- assertThat(capturedEvents.any { it.id == 621 }).isTrue()
- // pref_enable_minus_one true
+ assertThat(capturedEvents.any { it.id == LAUNCHER_NOTIFICATION_DOT_ENABLED.id }).isTrue()
+ assertThat(capturedEvents.any { it.id == LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON.id })
+ .isTrue()
+ assertThat(capturedEvents.any { it.id == LAUNCHER_THEMED_ICON_DISABLED.id }).isTrue()
+ assertThat(capturedEvents.any { it.id == LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED.id })
+ .isTrue()
+ assertThat(capturedEvents.any { it.id == LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED.id })
+ .isTrue()
+ assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED.id })
+ .isTrue()
+ // LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED
assertThat(capturedEvents.any { it.id == 617 }).isTrue()
}
}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 27e761a..07c4ffc 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -18,6 +18,7 @@
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.ComponentName
+import android.content.Context
import android.content.Intent
import android.os.Process
import android.os.UserHandle
@@ -56,6 +57,7 @@
@Mock private lateinit var mockIconCache: TaskIconCache
@Mock private lateinit var mockRecentsModel: RecentsModel
+ @Mock private lateinit var mockContext: Context
@Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
private var taskListChangeId: Int = 1
@@ -71,7 +73,9 @@
whenever(mockRecentsModel.iconCache).thenReturn(mockIconCache)
recentAppsController =
- TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController }
+ TaskbarRecentAppsController(mockContext, mockRecentsModel) {
+ mockDesktopVisibilityController
+ }
recentAppsController.init(taskbarControllers)
recentAppsController.canShowRunningApps = true
recentAppsController.canShowRecentApps = true
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
index d049fbc..c213dbb 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -29,6 +29,7 @@
import android.app.ActivityManager;
import android.app.KeyguardManager;
+import android.content.Context;
import androidx.test.filters.SmallTest;
@@ -54,7 +55,9 @@
public class RecentTasksListTest {
@Mock
- private SystemUiProxy mockSystemUiProxy;
+ private Context mContext;
+ @Mock
+ private SystemUiProxy mSystemUiProxy;
@Mock
private TopTaskTracker mTopTaskTracker;
@@ -66,14 +69,14 @@
MockitoAnnotations.initMocks(this);
LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class);
KeyguardManager mockKeyguardManager = mock(KeyguardManager.class);
- mRecentTasksList = new RecentTasksList(mockMainThreadExecutor, mockKeyguardManager,
- mockSystemUiProxy, mTopTaskTracker);
+ mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor,
+ mockKeyguardManager, mSystemUiProxy, mTopTaskTracker);
}
@Test
public void onRecentTasksChanged_doesNotFetchTasks() throws Exception {
mRecentTasksList.onRecentTasksChanged();
- verify(mockSystemUiProxy, times(0))
+ verify(mSystemUiProxy, times(0))
.getRecentTasks(anyInt(), anyInt());
}
@@ -81,7 +84,7 @@
public void loadTasksInBackground_onlyKeys_noValidTaskDescription() throws Exception {
GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(
new ActivityManager.RecentTaskInfo(), new ActivityManager.RecentTaskInfo(), null);
- when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1,
@@ -94,7 +97,7 @@
@Test
public void loadTasksInBackground_GetRecentTasksException() throws Exception {
- when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenThrow(new SystemUiProxy.GetRecentTasksException("task load failed"));
RecentTasksList.TaskLoadResult taskList = mRecentTasksList.loadTasksInBackground(
@@ -113,7 +116,7 @@
task2.taskDescription = new ActivityManager.TaskDescription();
GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(task1, task2,
null);
- when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1,
@@ -132,7 +135,7 @@
createRecentTaskInfo(5 /* taskId */)};
GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forFreeformTasks(
tasks, Collections.emptySet() /* minimizedTaskIds */);
- when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(
@@ -158,7 +161,7 @@
Arrays.stream(new Integer[]{1, 4, 5}).collect(Collectors.toSet());
GroupedRecentTaskInfo recentTaskInfos =
GroupedRecentTaskInfo.forFreeformTasks(tasks, minimizedTaskIds);
- when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
index ddd3042..ebfa996 100644
--- a/res/drawable/rounded_action_button.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -22,8 +22,5 @@
<stroke
android:width="1dp"
android:color="?attr/materialColorSurfaceContainerLow" />
- <padding
- android:left="@dimen/rounded_button_padding"
- android:right="@dimen/rounded_button_padding" />
</shape>
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index c581ae3..a45d585 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -44,8 +44,7 @@
<FrameLayout
android:layout_width="@dimen/rounded_button_width"
android:layout_height="@dimen/rounded_button_width"
- android:background="@drawable/rounded_action_button"
- android:padding="@dimen/rounded_button_padding">
+ android:background="@drawable/rounded_action_button">
<ImageButton
android:id="@+id/action_btn"
android:layout_width="@dimen/x_icon_size"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 05724e2..af91b5a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -172,8 +172,6 @@
<dimen name="padded_rounded_button_height">48dp</dimen>
<dimen name="rounded_button_height">48dp</dimen>
<dimen name="rounded_button_radius">200dp</dimen>
- <dimen name="rounded_button_padding">8dp</dimen>
-
<!-- Widget tray -->
<dimen name="widget_cell_vertical_padding">8dp</dimen>
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index a448228..53fed20 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -746,7 +746,8 @@
* | +--+ |
* | |
* +----------------+
- * This would be case delta % 4 == 2:
+ * This would be case delta % 4 == 2: // This is case was reverted to previous behaviour which
+ * doesn't match the illustration due to b/353965234
* +-------------+
* | |
* | |
@@ -768,7 +769,6 @@
int delta) {
int rdelta = ((delta % 4) + 4) % 4;
int origLeft = inOutBounds.left;
- int origTop = inOutBounds.top;
switch (rdelta) {
case 0:
return;
@@ -780,8 +780,6 @@
return;
case 2:
inOutBounds.left = parentWidth - inOutBounds.right;
- inOutBounds.top = parentHeight - inOutBounds.bottom;
- inOutBounds.bottom = parentHeight - origTop;
inOutBounds.right = parentWidth - origLeft;
return;
case 3:
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 89e6adc..a4f130a 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -262,11 +262,12 @@
writer.println(prefix + "\tAllAppsStore Apps[] size: " + mApps.length);
for (int i = 0; i < mApps.length; i++) {
writer.println(String.format(Locale.getDefault(),
- "%s\tPackage index, name, and class: " + "%d/%s:%s",
+ "%s\tPackage index, name, class, and description: %d/%s:%s, %s",
prefix,
i,
mApps[i].componentName.getPackageName(),
- mApps[i].componentName.getClassName()));
+ mApps[i].componentName.getClassName(),
+ mApps[i].contentDescription));
}
}
}
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index e44ea1d..a691e45 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -43,6 +43,7 @@
import android.view.animation.OvershootInterpolator;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
@@ -131,7 +132,8 @@
private float mCurrentPosition;
private float mFinalPosition;
private boolean mIsScrollPaused;
- private boolean mIsTwoPanels;
+ @VisibleForTesting
+ boolean mIsTwoPanels;
private ObjectAnimator mAnimator;
private @Nullable ObjectAnimator mAlphaAnimator;
@@ -477,6 +479,21 @@
return sTempRect;
}
+ @VisibleForTesting
+ int getActivePage() {
+ return mActivePage;
+ }
+
+ @VisibleForTesting
+ int getNumPages() {
+ return mNumPages;
+ }
+
+ @VisibleForTesting
+ float getCurrentPosition() {
+ return mCurrentPosition;
+ }
+
private class MyOutlineProver extends ViewOutlineProvider {
@Override
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index 24d58f3..c117be4 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -32,7 +32,6 @@
import androidx.annotation.WorkerThread;
import com.android.launcher3.Flags;
-import com.android.launcher3.Utilities;
import com.android.launcher3.util.PackageUserKey;
import java.lang.ref.WeakReference;
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 7339111..e861961 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -183,6 +183,11 @@
mUserToSerialMap.put(userHandle, info);
}
+ @VisibleForTesting
+ public void putToPreInstallCache(UserHandle userHandle, List<String> preInstalledApps) {
+ mUserToPreInstallAppMap.put(userHandle, preInstalledApps);
+ }
+
/**
* @see UserManager#getUserProfiles()
*/
diff --git a/src/com/android/launcher3/util/LogConfig.java b/src/com/android/launcher3/util/LogConfig.java
index 3d4b409..f183f18 100644
--- a/src/com/android/launcher3/util/LogConfig.java
+++ b/src/com/android/launcher3/util/LogConfig.java
@@ -76,4 +76,5 @@
* When turned on, we enable zero state web data loader related logging.
*/
public static final String ZERO_WEB_DATA_LOADER = "ZeroStateWebDataLoaderLog";
+ public static final String SEARCH_TARGET_UTIL_LOG = "SearchTargetUtilLog";
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt b/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
index 5a26087..d0aa7a8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
@@ -376,9 +376,10 @@
Utilities.rotateBounds(rect, 100, 100, 1)
assertEquals(Rect(70, 40, 80, 80), rect)
- rect = Rect(20, 70, 60, 80)
- Utilities.rotateBounds(rect, 100, 100, 2)
- assertEquals(Rect(40, 20, 80, 30), rect)
+ // case removed for b/28435189
+ // rect = Rect(20, 70, 60, 80)
+ // Utilities.rotateBounds(rect, 100, 100, 2)
+ // assertEquals(Rect(40, 20, 80, 30), rect)
rect = Rect(20, 70, 60, 80)
Utilities.rotateBounds(rect, 100, 100, 3)
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
index 1de99c5..d3e27b6 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
@@ -23,6 +23,7 @@
import android.os.Looper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
@@ -114,6 +115,7 @@
underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
underTest.register(context, completionRunnable, 1, "test_action_1", "test_action_2")
+ getInstrumentation().waitForIdleSync()
verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture(), eq(1))
verify(completionRunnable).run()
@@ -136,6 +138,7 @@
underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
underTest.unregisterReceiverSafely(context)
+ getInstrumentation().waitForIdleSync()
verify(context).unregisterReceiver(same(underTest))
}
diff --git a/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt b/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
new file mode 100644
index 0000000..b491f17
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
@@ -0,0 +1,201 @@
+/*
+ * 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.folder
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.folder.FolderNameInfos.*
+import org.junit.Test
+import org.junit.runner.RunWith
+
+data class Label(val index: Int, val label: String, val score: Float)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderNameInfosTest {
+
+ companion object {
+ val statusList =
+ listOf(
+ SUCCESS,
+ HAS_PRIMARY,
+ HAS_SUGGESTIONS,
+ ERROR_NO_PROVIDER,
+ ERROR_APP_LOOKUP_FAILED,
+ ERROR_ALL_APP_LOOKUP_FAILED,
+ ERROR_NO_LABELS_GENERATED,
+ ERROR_LABEL_LOOKUP_FAILED,
+ ERROR_ALL_LABEL_LOOKUP_FAILED,
+ ERROR_NO_PACKAGES,
+ )
+ }
+
+ @Test
+ fun status() {
+ assertStatus(statusList)
+ assertStatus(
+ listOf(
+ ERROR_NO_PROVIDER,
+ ERROR_APP_LOOKUP_FAILED,
+ ERROR_ALL_APP_LOOKUP_FAILED,
+ ERROR_NO_LABELS_GENERATED,
+ ERROR_LABEL_LOOKUP_FAILED,
+ ERROR_ALL_LABEL_LOOKUP_FAILED,
+ ERROR_NO_PACKAGES,
+ )
+ )
+ assertStatus(
+ listOf(
+ SUCCESS,
+ HAS_PRIMARY,
+ HAS_SUGGESTIONS,
+ )
+ )
+ assertStatus(
+ listOf(
+ SUCCESS,
+ HAS_PRIMARY,
+ HAS_SUGGESTIONS,
+ )
+ )
+ }
+
+ fun assertStatus(statusList: List<Int>) {
+ var infos = FolderNameInfos()
+ statusList.forEach { infos.setStatus(it) }
+ assert(infos.status() == statusList.sum()) {
+ "There is an overlap on the status constants!"
+ }
+ }
+
+ @Test
+ fun hasPrimary() {
+ assertHasPrimary(
+ createNameInfos(listOf(Label(0, "label", 1f)), statusList),
+ hasPrimary = true
+ )
+ assertHasPrimary(
+ createNameInfos(listOf(Label(1, "label", 1f)), statusList),
+ hasPrimary = false
+ )
+ assertHasPrimary(
+ createNameInfos(
+ listOf(Label(0, "label", 1f)),
+ listOf(
+ ERROR_NO_PROVIDER,
+ ERROR_APP_LOOKUP_FAILED,
+ ERROR_ALL_APP_LOOKUP_FAILED,
+ ERROR_NO_LABELS_GENERATED,
+ ERROR_LABEL_LOOKUP_FAILED,
+ ERROR_ALL_LABEL_LOOKUP_FAILED,
+ ERROR_NO_PACKAGES,
+ )
+ ),
+ hasPrimary = false
+ )
+ }
+
+ private fun assertHasPrimary(nameInfos: FolderNameInfos, hasPrimary: Boolean) =
+ assert(nameInfos.hasPrimary() == hasPrimary)
+
+ private fun createNameInfos(labels: List<Label>?, statusList: List<Int>?): FolderNameInfos {
+ val infos = FolderNameInfos()
+ labels?.forEach { infos.setLabel(it.index, it.label, it.score) }
+ statusList?.forEach { infos.setStatus(it) }
+ return infos
+ }
+
+ @Test
+ fun hasSuggestions() {
+ assertHasSuggestions(
+ createNameInfos(listOf(Label(0, "label", 1f)), null),
+ hasSuggestions = true
+ )
+ assertHasSuggestions(createNameInfos(null, null), hasSuggestions = false)
+ // There is a max of 4 suggestions
+ assertHasSuggestions(
+ createNameInfos(listOf(Label(5, "label", 1f)), null),
+ hasSuggestions = false
+ )
+ assertHasSuggestions(
+ createNameInfos(
+ listOf(
+ Label(0, "label", 1f),
+ Label(1, "label", 1f),
+ Label(2, "label", 1f),
+ Label(3, "label", 1f)
+ ),
+ null
+ ),
+ hasSuggestions = true
+ )
+ }
+
+ private fun assertHasSuggestions(nameInfos: FolderNameInfos, hasSuggestions: Boolean) =
+ assert(nameInfos.hasSuggestions() == hasSuggestions)
+
+ @Test
+ fun hasContains() {
+ assertContains(
+ createNameInfos(
+ listOf(
+ Label(0, "label1", 1f),
+ Label(1, "label2", 1f),
+ Label(2, "label3", 1f),
+ Label(3, "label4", 1f)
+ ),
+ null
+ ),
+ label = Label(-1, "label3", -1f),
+ contains = true
+ )
+ assertContains(
+ createNameInfos(
+ listOf(
+ Label(0, "label1", 1f),
+ Label(1, "label2", 1f),
+ Label(2, "label3", 1f),
+ Label(3, "label4", 1f)
+ ),
+ null
+ ),
+ label = Label(-1, "label5", -1f),
+ contains = false
+ )
+ assertContains(
+ createNameInfos(null, null),
+ label = Label(-1, "label1", -1f),
+ contains = false
+ )
+ assertContains(
+ createNameInfos(
+ listOf(
+ Label(0, "label1", 1f),
+ Label(1, "label2", 1f),
+ Label(2, "lAbel3", 1f),
+ Label(3, "lEbel4", 1f)
+ ),
+ null
+ ),
+ label = Label(-1, "LaBEl3", -1f),
+ contains = true
+ )
+ }
+
+ private fun assertContains(nameInfos: FolderNameInfos, label: Label, contains: Boolean) =
+ assert(nameInfos.contains(label.label) == contains)
+}
diff --git a/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
index da14425..5516f45 100644
--- a/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
+++ b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
@@ -23,7 +23,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
-import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.THEMED_ICONS
import com.android.launcher3.LauncherPrefs.Companion.get
import com.android.launcher3.icons.BaseIconFactory
import com.android.launcher3.icons.FastBitmapDrawable
@@ -51,6 +51,8 @@
private lateinit var modelHelper: LauncherModelHelper
private lateinit var folderIcon: FolderIcon
+ private var defaultThemedIcons = false
+
@Before
fun setup() {
getInstrumentation().runOnMainSync {
@@ -127,16 +129,20 @@
previewItemManager.mIconSize
)
)
+
+ defaultThemedIcons = get(context).get(THEMED_ICONS)
}
+
@After
@Throws(Exception::class)
fun tearDown() {
+ get(context).put(THEMED_ICONS, defaultThemedIcons)
modelHelper.destroy()
}
@Test
fun checkThemedIconWithThemingOn_iconShouldBeThemed() {
- get(context).put(LauncherPrefs.THEMED_ICONS, true)
+ get(context).put(THEMED_ICONS, true)
val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
previewItemManager.setDrawable(drawingParams, folderItems[0])
@@ -146,7 +152,7 @@
@Test
fun checkThemedIconWithThemingOff_iconShouldNotBeThemed() {
- get(context).put(LauncherPrefs.THEMED_ICONS, false)
+ get(context).put(THEMED_ICONS, false)
val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
previewItemManager.setDrawable(drawingParams, folderItems[0])
@@ -156,7 +162,7 @@
@Test
fun checkUnthemedIconWithThemingOn_iconShouldNotBeThemed() {
- get(context).put(LauncherPrefs.THEMED_ICONS, true)
+ get(context).put(THEMED_ICONS, true)
val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
previewItemManager.setDrawable(drawingParams, folderItems[1])
@@ -166,7 +172,7 @@
@Test
fun checkUnthemedIconWithThemingOff_iconShouldNotBeThemed() {
- get(context).put(LauncherPrefs.THEMED_ICONS, false)
+ get(context).put(THEMED_ICONS, false)
val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
previewItemManager.setDrawable(drawingParams, folderItems[1])
@@ -176,7 +182,7 @@
@Test
fun checkThemedIconWithBadgeWithThemingOn_iconAndBadgeShouldBeThemed() {
- get(context).put(LauncherPrefs.THEMED_ICONS, true)
+ get(context).put(THEMED_ICONS, true)
val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
previewItemManager.setDrawable(drawingParams, folderItems[2])
@@ -189,7 +195,7 @@
@Test
fun checkUnthemedIconWithBadgeWithThemingOn_badgeShouldBeThemed() {
- get(context).put(LauncherPrefs.THEMED_ICONS, true)
+ get(context).put(THEMED_ICONS, true)
val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
previewItemManager.setDrawable(drawingParams, folderItems[3])
@@ -202,7 +208,7 @@
@Test
fun checkUnthemedIconWithBadgeWithThemingOff_iconAndBadgeShouldNotBeThemed() {
- get(context).put(LauncherPrefs.THEMED_ICONS, false)
+ get(context).put(THEMED_ICONS, false)
val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
previewItemManager.setDrawable(drawingParams, folderItems[3])
diff --git a/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
new file mode 100644
index 0000000..9a8f957
--- /dev/null
+++ b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.pageindicators
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.ActivityContextWrapper
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+import org.mockito.Mockito
+
+class PageIndicatorDotsTest {
+
+ private val context: Context =
+ ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+ private val pageIndicatorDots: PageIndicatorDots = Mockito.spy(PageIndicatorDots(context))
+
+ @Test
+ fun `setActiveMarker should set the active page to the parameter passed`() {
+ pageIndicatorDots.setActiveMarker(2)
+
+ assertEquals(2, pageIndicatorDots.activePage)
+ }
+
+ @Test
+ fun `setActiveMarker should set the active page to the parameter passed divided by two in two panel layouts`() {
+ pageIndicatorDots.mIsTwoPanels = true
+
+ pageIndicatorDots.setActiveMarker(5)
+
+ assertEquals(2, pageIndicatorDots.activePage)
+ }
+
+ @Test
+ fun `setMarkersCount should set the number of pages to the passed parameter and if the last page gets removed we want to go to the previous page`() {
+ pageIndicatorDots.setMarkersCount(3)
+
+ assertEquals(3, pageIndicatorDots.numPages)
+ }
+
+ @Test
+ fun `for setMarkersCount if the last page gets removed we want to go to the previous page`() {
+ pageIndicatorDots.setActiveMarker(2)
+
+ pageIndicatorDots.setMarkersCount(2)
+
+ assertEquals(1, pageIndicatorDots.activePage)
+ assertEquals(pageIndicatorDots.activePage.toFloat(), pageIndicatorDots.currentPosition)
+ }
+}
diff --git a/tests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt b/tests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
new file mode 100644
index 0000000..b531adb
--- /dev/null
+++ b/tests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
@@ -0,0 +1,228 @@
+/*
+ * 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.pm
+
+import android.content.pm.LauncherApps
+import android.content.pm.PackageInstaller
+import android.os.Build
+import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.PackageUserKey
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InstallSessionTrackerTest {
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val mockInstallSessionHelper: InstallSessionHelper = mock()
+ private val mockCallback: InstallSessionTracker.Callback = mock()
+ private val mockPackageInstaller: PackageInstaller = mock()
+
+ private val launcherModelHelper = LauncherModelHelper()
+ private val sandboxContext = launcherModelHelper.sandboxContext
+
+ lateinit var launcherApps: LauncherApps
+ lateinit var installSessionTracker: InstallSessionTracker
+
+ @Before
+ fun setup() {
+ launcherApps = sandboxContext.spyService(LauncherApps::class.java)
+ installSessionTracker =
+ InstallSessionTracker(
+ mockInstallSessionHelper,
+ mockCallback,
+ mockPackageInstaller,
+ launcherApps
+ )
+ }
+
+ @After
+ fun teardown() {
+ launcherModelHelper.destroy()
+ }
+
+ @Test
+ fun `onCreated triggers callbacks for setting up new install session`() {
+ // Given
+ val expectedSessionId = 1
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = expectedSessionId
+ appPackageName = "appPackageName"
+ userId = 0
+ }
+ val expectedPackageKey = PackageUserKey("appPackageName", UserHandle(0))
+ whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+ .thenReturn(expectedSession)
+ // When
+ installSessionTracker.onCreated(expectedSessionId)
+ // Then
+ verify(mockCallback).onInstallSessionCreated(any())
+ verify(mockCallback).onUpdateSessionDisplay(expectedPackageKey, expectedSession)
+ verify(mockInstallSessionHelper).tryQueuePromiseAppIcon(expectedSession)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
+ fun `onCreated for unarchival triggers onPackageStateChanged`() {
+ // Given
+ val expectedSessionId = 1
+ val expectedSession =
+ spy(PackageInstaller.SessionInfo()).apply {
+ sessionId = expectedSessionId
+ appPackageName = "appPackageName"
+ userId = 0
+ whenever(isUnarchival).thenReturn(true)
+ }
+ whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+ .thenReturn(expectedSession)
+ // When
+ installSessionTracker.onCreated(expectedSessionId)
+ // Then
+ verify(mockCallback).onPackageStateChanged(any())
+ }
+
+ @Test
+ fun `onFinished triggers onPackageStateChanged if session found in cache`() {
+ // Given
+ val expectedSessionId = 1
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = expectedSessionId
+ appPackageName = "appPackageName"
+ userId = 0
+ }
+ val expectedPackageKey = PackageUserKey("appPackageName", UserHandle(0))
+ whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+ .thenReturn(expectedSession)
+ whenever(mockInstallSessionHelper.activeSessions)
+ .thenReturn(hashMapOf(expectedPackageKey to expectedSession))
+ // When
+ installSessionTracker.onFinished(expectedSessionId, /* success */ true)
+ // Then
+ verify(mockCallback).onPackageStateChanged(any())
+ }
+
+ @Test
+ fun `onFinished failure calls onSessionFailure and promise icon removal for existing icon`() {
+ // Given
+ val expectedSessionId = 1
+ val expectedPackage = "appPackageName"
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = expectedSessionId
+ appPackageName = expectedPackage
+ userId = 0
+ }
+ val expectedPackageKey = PackageUserKey(expectedPackage, UserHandle(0))
+ whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+ .thenReturn(expectedSession)
+ whenever(mockInstallSessionHelper.activeSessions)
+ .thenReturn(hashMapOf(expectedPackageKey to expectedSession))
+ whenever(mockInstallSessionHelper.promiseIconAddedForId(expectedSessionId)).thenReturn(true)
+ // When
+ installSessionTracker.onFinished(expectedSessionId, /* success */ false)
+ // Then
+ verify(mockCallback).onSessionFailure(expectedPackage, expectedPackageKey.mUser)
+ verify(mockInstallSessionHelper).removePromiseIconId(expectedSessionId)
+ }
+
+ @Test
+ fun `onProgressChanged triggers onPackageStateChanged if verified session found`() {
+ // Given
+ val expectedSessionId = 1
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = expectedSessionId
+ appPackageName = "appPackageName"
+ userId = 0
+ }
+ whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+ .thenReturn(expectedSession)
+ // When
+ installSessionTracker.onProgressChanged(expectedSessionId, /* progress */ 50f)
+ // Then
+ verify(mockCallback).onPackageStateChanged(any())
+ }
+
+ @Test
+ fun `onBadgingChanged triggers session display update and queues promise icon if verified`() {
+ // Given
+ val expectedSessionId = 1
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = expectedSessionId
+ appPackageName = "appPackageName"
+ userId = 0
+ }
+ val expectedPackageKey = PackageUserKey("appPackageName", UserHandle(0))
+ whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+ .thenReturn(expectedSession)
+ // When
+ installSessionTracker.onBadgingChanged(expectedSessionId)
+ // Then
+ verify(mockCallback).onUpdateSessionDisplay(expectedPackageKey, expectedSession)
+ verify(mockInstallSessionHelper).tryQueuePromiseAppIcon(expectedSession)
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ fun `register triggers registerPackageInstallerSessionCallback for versions from Q`() {
+ // Given
+ whenever(
+ launcherApps.registerPackageInstallerSessionCallback(
+ MODEL_EXECUTOR,
+ installSessionTracker
+ )
+ )
+ .then { /* no-op */ }
+ // When
+ installSessionTracker.register()
+ // Then
+ verify(launcherApps)
+ .registerPackageInstallerSessionCallback(MODEL_EXECUTOR, installSessionTracker)
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ fun `unregister triggers unregisterPackageInstallerSessionCallback for versions from Q`() {
+ // Given
+ whenever(launcherApps.unregisterPackageInstallerSessionCallback(installSessionTracker))
+ .then { /* no-op */ }
+ // When
+ installSessionTracker.unregister()
+ // Then
+ verify(launcherApps).unregisterPackageInstallerSessionCallback(installSessionTracker)
+ }
+}
diff --git a/tests/src/com/android/launcher3/pm/UserCacheTest.kt b/tests/src/com/android/launcher3/pm/UserCacheTest.kt
new file mode 100644
index 0000000..b21219e
--- /dev/null
+++ b/tests/src/com/android/launcher3/pm/UserCacheTest.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.pm
+
+import android.os.Process.myUserHandle
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.TestUtil
+import com.android.launcher3.util.UserIconInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UserCacheTest {
+ private val launcherModelHelper = LauncherModelHelper()
+ private val sandboxContext = launcherModelHelper.sandboxContext
+ private lateinit var userCache: UserCache
+
+ @Before
+ fun setup() {
+ userCache = UserCache.getInstance(sandboxContext)
+ }
+
+ @After
+ fun teardown() {
+ launcherModelHelper.destroy()
+ }
+
+ @Test
+ fun `getBadgeDrawable only returns a UserBadgeDrawable given a user in the cache`() {
+ // Given
+ val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK)
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToCache(myUserHandle(), expectedIconInfo)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualDrawable = UserCache.getBadgeDrawable(sandboxContext, myUserHandle())
+ val unexpectedDrawable = UserCache.getBadgeDrawable(sandboxContext, UserHandle(66))
+ // Then
+ assertThat(actualDrawable).isNotNull()
+ assertThat(unexpectedDrawable).isNull()
+ }
+
+ @Test
+ fun `getPreInstallApps returns list of pre installed apps given a user`() {
+ // Given
+ val expectedApps = listOf("Google")
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToPreInstallCache(myUserHandle(), expectedApps)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualApps = userCache.getPreInstallApps(myUserHandle())
+ // Then
+ assertThat(actualApps).isEqualTo(expectedApps)
+ }
+
+ @Test
+ fun `getUserProfiles returns copy of UserCache profiles`() {
+ // Given
+ val expectedProfiles = listOf(myUserHandle())
+ val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_MAIN)
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToCache(myUserHandle(), expectedIconInfo)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualProfiles = userCache.userProfiles
+ // Then
+ assertThat(actualProfiles).isEqualTo(expectedProfiles)
+ }
+
+ @Test
+ fun `getUserForSerialNumber returns user key matching given entry serial number`() {
+ // Given
+ val expectedSerial = 42L
+ val expectedProfile = UserHandle(42)
+ val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_MAIN, expectedSerial)
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToCache(expectedProfile, expectedIconInfo)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualProfile = userCache.getUserForSerialNumber(expectedSerial)
+ // Then
+ assertThat(actualProfile).isEqualTo(expectedProfile)
+ }
+
+ @Test
+ fun `getUserInfo returns cached UserIconInfo given user key`() {
+ // Given
+ val expectedProfile = UserHandle(1)
+ val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK)
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToCache(expectedProfile, expectedIconInfo)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualIconInfo = userCache.getUserInfo(expectedProfile)
+ // Then
+ assertThat(actualIconInfo).isEqualTo(expectedIconInfo)
+ }
+
+ @Test
+ fun `getSerialNumberForUser returns cached UserIconInfo serial number given user key`() {
+ // Given
+ val expectedSerial = 42L
+ val expectedProfile = UserHandle(1)
+ val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK, expectedSerial)
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToCache(expectedProfile, expectedIconInfo)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualSerial = userCache.getSerialNumberForUser(expectedProfile)
+ // Then
+ assertThat(actualSerial).isEqualTo(expectedSerial)
+ }
+}