Merge "Fix swipe up from Desktop Tasks is updated does not show focused task" into main
diff --git a/quickstep/res/values/ids.xml b/quickstep/res/values/ids.xml
index 3091d9e..c71bb76 100644
--- a/quickstep/res/values/ids.xml
+++ b/quickstep/res/values/ids.xml
@@ -19,4 +19,6 @@
<item type="id" name="action_move_left" />
<item type="id" name="action_move_right" />
<item type="id" name="action_dismiss_all" />
+
+ <item type="id" name="bubble_bar_flyout_view" />
</resources>
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index 94a1814..0f3aaa6 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -51,7 +51,6 @@
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.util.DaggerSingletonObject;
import com.android.launcher3.util.DaggerSingletonTracker;
-import com.android.launcher3.util.ExecutorUtil;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.SafeCloseable;
@@ -122,7 +121,7 @@
}
};
mWorkerHandler.post(this::initializeInBackground);
- ExecutorUtil.executeSyncOnMainOrFail(() -> tracker.addCloseable(this));
+ tracker.addCloseable(this);
}
@WorkerThread
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index fc8204a..50a253c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -17,8 +17,6 @@
import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID;
-import static com.android.launcher3.taskbar.KeyboardQuickSwitchController.MAX_TASKS;
-
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -200,7 +198,7 @@
View previousTaskView = null;
LayoutInflater layoutInflater = LayoutInflater.from(context);
- int tasksToDisplay = Math.min(MAX_TASKS, groupTasks.size());
+ int tasksToDisplay = groupTasks.size();
for (int i = 0; i < tasksToDisplay; i++) {
GroupTask groupTask = groupTasks.get(i);
KeyboardQuickSwitchTaskView currentTaskView = createAndAddTaskView(
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 0b850bd..042bc9a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -154,6 +154,12 @@
}
}
+ @Override
+ protected boolean isTaskbarTouchable() {
+ return !(mTaskbarLauncherStateController.isAnimatingToLauncher()
+ && mTaskbarLauncherStateController.isTaskbarAlignedWithHotseat());
+ }
+
public void setShouldDelayLauncherStateAnim(boolean shouldDelayLauncherStateAnim) {
mTaskbarLauncherStateController.setShouldDelayLauncherStateAnim(
shouldDelayLauncherStateAnim);
@@ -447,16 +453,6 @@
}
@Override
- public boolean isHotseatVisibleForTaskBarAlignment() {
- return mTaskbarLauncherStateController.isHotseatVisibleForTaskbarAlignment();
- }
-
- @Override
- public boolean isAnimatingToLauncher() {
- return mTaskbarLauncherStateController.isAnimatingToLauncher();
- }
-
- @Override
protected boolean isInOverviewUi() {
return mTaskbarLauncherStateController.isInOverviewUi();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
index 69bc6bd..3f6ebe2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
@@ -134,10 +134,31 @@
super.orientAboutObject()
x =
if (Flags.showTaskbarPinningPopupFromAnywhere()) {
- min(
- max(minPaddingFromScreenEdge, horizontalPosition - measuredWidth / 2f),
- popupContainer.getWidth() - measuredWidth - minPaddingFromScreenEdge,
- )
+ val xForCenterAlignment = horizontalPosition - measuredWidth / 2f
+ val maxX = popupContainer.getWidth() - measuredWidth - minPaddingFromScreenEdge
+ when {
+ // Left-aligned popup and its arrow pointing to the event position if there is
+ // not enough space to center it.
+ xForCenterAlignment < minPaddingFromScreenEdge ->
+ max(
+ minPaddingFromScreenEdge,
+ horizontalPosition - mArrowOffsetHorizontal - mArrowWidth / 2,
+ )
+
+ // Right-aligned popup and its arrow pointing to the event position if there
+ // is not enough space to center it.
+ xForCenterAlignment > maxX ->
+ min(
+ horizontalPosition - measuredWidth +
+ mArrowOffsetHorizontal +
+ mArrowWidth / 2,
+ popupContainer.getWidth() - measuredWidth - minPaddingFromScreenEdge,
+ )
+
+ // Default alignment where the popup and its arrow are centered relative to the
+ // event position.
+ else -> xForCenterAlignment
+ }
} else {
mTempRect.centerX() - measuredWidth / 2f
}
@@ -183,7 +204,17 @@
override fun addArrow() {
super.addArrow()
if (Flags.showTaskbarPinningPopupFromAnywhere()) {
- mArrow.x = horizontalPosition - mArrowWidth / 2
+ mArrow.x =
+ min(
+ max(
+ minPaddingFromScreenEdge + mArrowOffsetHorizontal,
+ horizontalPosition - mArrowWidth / 2,
+ ),
+ popupContainer.getWidth() -
+ minPaddingFromScreenEdge -
+ mArrowOffsetHorizontal -
+ mArrowWidth,
+ )
} else {
val location = IntArray(2)
popupContainer.getLocationInDragLayer(dividerView, location)
@@ -232,7 +263,7 @@
/** Aligning the view pivot to center for animation. */
override fun setPivotForOpenCloseAnimation() {
- pivotX = measuredWidth / 2f
+ pivotX = mArrow.x + mArrowWidth / 2 - x
pivotY = measuredHeight.toFloat()
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 55722a9..058dd07 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -47,7 +47,6 @@
import com.android.internal.policy.GestureNavigationSettingsObserver
import com.android.launcher3.DeviceProfile
import com.android.launcher3.R
-import com.android.launcher3.Utilities
import com.android.launcher3.anim.AlphaUpdateListener
import com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION
import com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate
@@ -134,6 +133,9 @@
}
val bubbleControllers = controllers.bubbleControllers.getOrNull()
+ val taskbarTouchableHeight = taskbarStashController.touchableHeight
+ val bubblesTouchableHeight =
+ bubbleControllers?.bubbleStashController?.getTouchableHeight() ?: 0
// reset touch bounds
defaultTouchableRegion.setEmpty()
if (bubbleControllers != null) {
@@ -145,41 +147,16 @@
defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.bubbleBarBounds)
}
}
- val uiController = controllers.uiController
if (
- !uiController.isHotseatVisibleForTaskBarAlignment ||
+ taskbarStashController.isInApp ||
+ taskbarStashController.isInOverview ||
DisplayController.showLockedTaskbarOnHome(context)
) {
- // adding the taskbar touch region
- var left = 0
- var right = context.deviceProfile.widthPx
- val touchableHeight: Int
- if (uiController.isAnimatingToLauncher) {
- val dp = controllers.taskbarActivityContext.deviceProfile
- touchableHeight = windowLayoutParams.height
- if (dp.isQsbInline) {
- // if Qsb is inline need to exclude search icon from touch region
- val isRtl = Utilities.isRtl(context.resources)
- val navBarOffset =
- bubbleControllers?.bubbleBarViewController?.let {
- val isBubblesOnLeft = it.bubbleBarLocation.isOnLeft(isRtl)
- dp.getHotseatTranslationXForNavBar(context, isBubblesOnLeft)
- } ?: 0
- val hotseatPadding: Rect = dp.getHotseatLayoutPadding(context)
- val borderSpacing: Int = dp.hotseatBorderSpace
- if (isRtl) {
- right = dp.widthPx - hotseatPadding.right + borderSpacing + navBarOffset
- } else {
- left = hotseatPadding.left - borderSpacing + navBarOffset
- }
- }
- } else {
- // if not animating to launcher use the taskbar touchanle height
- touchableHeight = taskbarStashController.touchableHeight
- }
+ // only add the taskbar touch region if not on home
val bottom = windowLayoutParams.height
- val top = bottom - touchableHeight
- defaultTouchableRegion.addBoundsToRegion(Rect(left, top, right, bottom))
+ val top = bottom - taskbarTouchableHeight
+ val right = context.deviceProfile.widthPx
+ defaultTouchableRegion.addBoundsToRegion(Rect(/* left= */ 0, top, right, bottom))
}
// Pre-calculate insets for different providers across different rotations for this gravity
@@ -391,6 +368,10 @@
// Let touches pass through us.
insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
debugTouchableRegion.lastSetTouchableReason = "Stashed over IME"
+ } else if (!controllers.uiController.isTaskbarTouchable) {
+ // Let touches pass through us.
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+ debugTouchableRegion.lastSetTouchableReason = "Taskbar is not touchable"
} else if (controllers.taskbarDragController.isSystemDragInProgress) {
// Let touches pass through us.
insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 4bc7d3c..fa04739 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -190,8 +190,6 @@
private boolean mIsQsbInline;
- private boolean mIsHotseatVisibleForTaskbarAlignment;
-
private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener =
new DeviceProfile.OnDeviceProfileChangeListener() {
@Override
@@ -733,7 +731,6 @@
boolean committed) {
boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
TaskbarStashController stashController = mControllers.taskbarStashController;
- TaskbarInsetsController insetsController = mControllers.taskbarInsetsController;
stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, isInStashedState);
Animator stashAnimator = stashController.createApplyStateAnimator(duration);
if (stashAnimator != null) {
@@ -742,11 +739,7 @@
public void onAnimationEnd(Animator animation) {
if (isInStashedState && committed) {
// Reset hotseat alpha to default
- updateIconAlphaForHome(
- /* taskbarAlpha = */ 0,
- ALPHA_CHANNEL_TASKBAR_ALIGNMENT,
- /* updateTaskbarAlpha = */ false
- );
+ mLauncher.getHotseat().setIconsAlpha(1, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
}
}
@@ -871,14 +864,6 @@
if (mIsQsbInline) {
mLauncher.getHotseat().setQsbAlpha(targetAlpha, alphaChannel);
}
- if (alphaChannel == ALPHA_CHANNEL_TASKBAR_ALIGNMENT) {
- boolean isHotseatVisibleForTaskbarAlignment = isHotseatVisibleForTaskbarAlignment();
- if (mIsHotseatVisibleForTaskbarAlignment != isHotseatVisibleForTaskbarAlignment) {
- mIsHotseatVisibleForTaskbarAlignment = isHotseatVisibleForTaskbarAlignment;
- mControllers.taskbarInsetsController
- .onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
- }
- }
}
/** Updates launcher home screen appearance accordingly to the bubble bar location. */
@@ -939,13 +924,6 @@
translationXAnimation.start();
}
- /** Returns true if hotseat icons visible for the taskbar alignment */
- public boolean isHotseatVisibleForTaskbarAlignment() {
- return mLauncher.getHotseat()
- .getIconsAlpha(ALPHA_CHANNEL_TASKBAR_ALIGNMENT).getValue() == 1;
- }
-
-
private final class TaskBarRecentsAnimationListener implements
RecentsAnimationCallbacks.RecentsAnimationListener {
private final RecentsAnimationCallbacks mCallbacks;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index db69e8f..7030088 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -75,14 +75,8 @@
mControllers = null;
}
- /** Returns true if transition animation to launcher home is being played. */
- public boolean isAnimatingToLauncher() {
- return false;
- }
-
- /** Returns true if hotseat icons visible for the taskbar alignment. */
- public boolean isHotseatVisibleForTaskBarAlignment() {
- return false;
+ protected boolean isTaskbarTouchable() {
+ return true;
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index c0891a9..fcb583a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -185,6 +185,7 @@
resources.getDrawable(R.drawable.taskbar_overflow_icon));
mTaskbarOverflowView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
}
+ // TODO: Disable touch events on QSB otherwise it can crash.
mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
mMaxNumIcons = calculateMaxNumIcons();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 51e09ab..b22fd6f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -377,8 +377,6 @@
// Updates mean the dot state may have changed; any other changes were updated in
// the populateBubble step.
BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
- // If we're not stashed, we're visible so animate
- bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
mBubbleBarViewController.animateBubbleNotification(
bb, /* isExpanding= */ false, /* isUpdate= */ true);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 63f101f..76d3606 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -148,7 +148,8 @@
mBubbleBarFlyoutController = new BubbleBarFlyoutController(
mBubbleBarContainer, createFlyoutPositioner(), createFlyoutTopBoundaryListener());
mBubbleBarViewAnimator = new BubbleBarViewAnimator(
- mBarView, mBubbleStashController, mBubbleBarController::showExpandedView);
+ mBarView, mBubbleStashController, mBubbleBarFlyoutController,
+ mBubbleBarController::showExpandedView);
mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider;
onBubbleBarConfigurationChanged(/* animate= */ false);
mActivity.addOnDeviceProfileChangeListener(
@@ -781,6 +782,11 @@
/** Animates the bubble bar to notify the user about a bubble change. */
public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding,
boolean isUpdate) {
+ // if we're expanded, don't animate the bubble bar. just show the notification dot.
+ if (isExpanded()) {
+ bubble.getView().updateDotVisibility(/* animate= */ true);
+ return;
+ }
boolean isInApp = mTaskbarStashController.isInApp();
// if this is the first bubble, animate to the initial state.
if (mBarView.getBubbleChildCount() == 1 && !isUpdate) {
@@ -789,13 +795,12 @@
}
boolean persistentTaskbarOrOnHome = mBubbleStashController.isBubblesShowingOnHome()
|| !mBubbleStashController.isTransientTaskBar();
- if (persistentTaskbarOrOnHome && !isExpanded()) {
+ if (persistentTaskbarOrOnHome) {
mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding);
return;
}
- // only animate the new bubble if we're in an app, have handle view and not auto expanding
- if (isInApp && mBubbleStashController.getHasHandleView() && !isExpanded()) {
+ if (isInApp && mBubbleStashController.getHasHandleView()) {
mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding);
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 707655c..4f3e1ae 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -301,7 +301,7 @@
void updateDotVisibility(boolean animate) {
if (mDotSuppressedForBubbleUpdate) {
- // if the dot is suppressed for
+ // if the dot is suppressed for an update, there's nothing to do
return;
}
final float targetScale = hasUnseenContent() ? 1f : 0f;
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 6a955d9..8a52ca9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -27,6 +27,8 @@
import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
import com.android.launcher3.taskbar.bubbles.BubbleBarView
import com.android.launcher3.taskbar.bubbles.BubbleView
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
import com.android.wm.shell.shared.animation.PhysicsAnimator
@@ -36,8 +38,9 @@
constructor(
private val bubbleBarView: BubbleBarView,
private val bubbleStashController: BubbleStashController,
+ private val bubbleBarFlyoutController: BubbleBarFlyoutController,
private val onExpanded: Runnable,
- private val scheduler: Scheduler = HandlerScheduler(bubbleBarView)
+ private val scheduler: Scheduler = HandlerScheduler(bubbleBarView),
) {
private var animatingBubble: AnimatingBubble? = null
@@ -54,7 +57,7 @@
private companion object {
/** The time to show the flyout. */
- const val FLYOUT_DELAY_MS: Long = 2500
+ const val FLYOUT_DELAY_MS: Long = 3000
/** The initial scale Y value that the new bubble is set to before the animation starts. */
const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f
/** The minimum alpha value to make the bubble bar touchable. */
@@ -69,7 +72,7 @@
val showAnimation: Runnable,
val hideAnimation: Runnable,
val expand: Boolean,
- val state: State = State.CREATED
+ val state: State = State.CREATED,
) {
/**
@@ -91,7 +94,7 @@
/** The bubble notification is now fully showing and waiting to be hidden. */
IN,
/** The bubble notification is animating out. */
- ANIMATING_OUT
+ ANIMATING_OUT,
}
}
@@ -127,7 +130,7 @@
private val springConfig =
PhysicsAnimator.SpringConfig(
stiffness = SpringForce.STIFFNESS_LOW,
- dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
+ dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
)
/** Animates a bubble for the state where the bubble bar is stashed. */
@@ -137,8 +140,9 @@
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
- // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
- // and the second part hides it after a delay.
+ // the animation of a new bubble is divided into 2 parts. The first part transforms the
+ // handle to the bubble bar and then shows the flyout. The second part hides the flyout and
+ // transforms the bubble bar back to the handle.
val showAnimation = buildHandleToBubbleBarAnimation()
val hideAnimation = if (isExpanding) Runnable {} else buildBubbleBarToHandleAnimation()
animatingBubble =
@@ -243,7 +247,8 @@
cancelHideAnimation()
return@addEndListener
}
- moveToState(AnimatingBubble.State.IN)
+ setupAndShowFlyout()
+
// the bubble bar is now fully settled in. update taskbar touch region so it's touchable
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -316,7 +321,17 @@
bubbleBarView.scaleY = 1f
bubbleStashController.updateTaskbarTouchRegion()
}
- animator.start()
+
+ val bubble = animatingBubble?.bubbleView?.bubble as? BubbleBarBubble
+ val flyout = bubble?.flyoutMessage
+ if (flyout != null) {
+ bubbleBarFlyoutController.collapseFlyout {
+ onFlyoutRemoved(bubble.view)
+ animator.start()
+ }
+ } else {
+ animator.start()
+ }
}
/** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
@@ -326,16 +341,16 @@
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
- // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
- // and the second part hides it after a delay if we are in an app.
+ // the animation of a new bubble is divided into 2 parts. The first part slides in the
+ // bubble bar and shows the flyout. The second part hides the flyout and transforms the
+ // bubble bar to the handle if we're in an app.
val showAnimation = buildBubbleBarSpringInAnimation()
val hideAnimation =
if (isInApp && !isExpanding) {
buildBubbleBarToHandleAnimation()
} else {
- // in this case the bubble bar remains visible so not much to do. once we implement
- // the flyout we'll update this runnable to hide it.
Runnable {
+ bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubbleView) }
animatingBubble = null
bubbleStashController.showBubbleBarImmediate()
bubbleStashController.updateTaskbarTouchRegion()
@@ -370,7 +385,7 @@
if (animatingBubble?.expand == true) {
cancelHideAnimation()
} else {
- moveToState(AnimatingBubble.State.IN)
+ setupAndShowFlyout()
}
// the bubble bar is now fully settled in. update taskbar touch region so it's touchable
bubbleStashController.updateTaskbarTouchRegion()
@@ -384,8 +399,10 @@
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
+ // first bounce the bubble bar and show the flyout. Then hide the flyout.
val showAnimation = buildBubbleBarBounceAnimation()
val hideAnimation = Runnable {
+ bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubbleView) }
animatingBubble = null
bubbleStashController.showBubbleBarImmediate()
bubbleStashController.updateTaskbarTouchRegion()
@@ -413,7 +430,7 @@
expandBubbleBar()
cancelHideAnimation()
} else {
- moveToState(AnimatingBubble.State.IN)
+ setupAndShowFlyout()
}
}
@@ -427,10 +444,38 @@
.start()
}
+ private fun setupAndShowFlyout() {
+ val bubbleView = animatingBubble?.bubbleView
+ val bubble = bubbleView?.bubble as? BubbleBarBubble
+ val flyout = bubble?.flyoutMessage
+ if (flyout != null) {
+ bubbleView.suppressDotForBubbleUpdate(true)
+ bubbleBarFlyoutController.setUpAndShowFlyout(
+ BubbleBarFlyoutMessage(flyout.icon, flyout.title, flyout.message)
+ ) {
+ moveToState(AnimatingBubble.State.IN)
+ bubbleStashController.updateTaskbarTouchRegion()
+ }
+ } else {
+ moveToState(AnimatingBubble.State.IN)
+ }
+ }
+
+ private fun cancelFlyout() {
+ val bubbleView = animatingBubble?.bubbleView
+ bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved(bubbleView) }
+ }
+
+ private fun onFlyoutRemoved(bubbleView: BubbleView?) {
+ bubbleView?.suppressDotForBubbleUpdate(false)
+ bubbleStashController.updateTaskbarTouchRegion()
+ }
+
/** Handles touching the animating bubble bar. */
fun onBubbleBarTouchedWhileAnimating() {
PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning()
bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
+ cancelFlyout()
val hideAnimation = animatingBubble?.hideAnimation ?: return
scheduler.cancel(hideAnimation)
bubbleBarView.relativePivotY = 1f
@@ -439,6 +484,7 @@
/** Notifies the animator that the taskbar area was touched during an animation. */
fun onStashStateChangingWhileAnimating() {
+ cancelFlyout()
val hideAnimation = animatingBubble?.hideAnimation ?: return
scheduler.cancel(hideAnimation)
animatingBubble = null
@@ -446,7 +492,7 @@
bubbleBarView.relativePivotY = 1f
bubbleStashController.onNewBubbleAnimationInterrupted(
/* isStashed= */ bubbleBarView.alpha == 0f,
- bubbleBarView.translationY
+ bubbleBarView.translationY,
)
}
@@ -455,6 +501,7 @@
this.animatingBubble = animatingBubble.copy(expand = true)
// if we're fully in and waiting to hide, cancel the hide animation and clean up
if (animatingBubble.state == AnimatingBubble.State.IN) {
+ cancelFlyout()
expandBubbleBar()
cancelHideAnimation()
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
index c431deb..d6400bb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -21,8 +21,8 @@
import android.widget.FrameLayout
import androidx.core.animation.ValueAnimator
import com.android.launcher3.R
+import com.android.systemui.util.addListener
import com.android.systemui.util.doOnEnd
-import com.android.systemui.util.doOnStart
/** Creates and manages the visibility of the [BubbleBarFlyoutView]. */
class BubbleBarFlyoutController
@@ -35,14 +35,19 @@
) {
private companion object {
- const val EXPAND_COLLAPSE_ANIMATION_DURATION_MS = 250L
+ const val ANIMATION_DURATION_MS = 250L
}
private var flyout: BubbleBarFlyoutView? = null
private val horizontalMargin =
container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin)
- fun setUpFlyout(message: BubbleBarFlyoutMessage) {
+ private enum class AnimationType {
+ COLLAPSE,
+ FADE,
+ }
+
+ fun setUpAndShowFlyout(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
flyout?.let(container::removeView)
val flyout = BubbleBarFlyoutView(container.context, positioner, flyoutScheduler)
@@ -58,27 +63,42 @@
lp.marginEnd = horizontalMargin
container.addView(flyout, lp)
- val animator =
- ValueAnimator.ofFloat(0f, 1f).setDuration(EXPAND_COLLAPSE_ANIMATION_DURATION_MS)
+ val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
animator.addUpdateListener { _ ->
flyout.updateExpansionProgress(animator.animatedValue as Float)
}
- animator.doOnStart {
- val flyoutTop = flyout.top + flyout.translationY
- // If the top position of the flyout is negative, then it's bleeding over the
- // top boundary of its parent view
- if (flyoutTop < 0) topBoundaryListener.extendTopBoundary(space = -flyoutTop.toInt())
- }
+ animator.addListener(
+ onStart = {
+ val flyoutTop = flyout.top + flyout.translationY
+ // If the top position of the flyout is negative, then it's bleeding over the
+ // top boundary of its parent view
+ if (flyoutTop < 0) topBoundaryListener.extendTopBoundary(space = -flyoutTop.toInt())
+ },
+ onEnd = { onEnd() },
+ )
flyout.showFromCollapsed(message) { animator.start() }
this.flyout = flyout
}
- fun hideFlyout(endAction: () -> Unit) {
+ fun cancelFlyout(endAction: () -> Unit) {
+ hideFlyout(AnimationType.FADE, endAction)
+ }
+
+ fun collapseFlyout(endAction: () -> Unit) {
+ hideFlyout(AnimationType.COLLAPSE, endAction)
+ }
+
+ private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) {
+ // TODO: b/277815200 - stop the current animation if it's running
val flyout = this.flyout ?: return
- val animator =
- ValueAnimator.ofFloat(1f, 0f).setDuration(EXPAND_COLLAPSE_ANIMATION_DURATION_MS)
- animator.addUpdateListener { _ ->
- flyout.updateExpansionProgress(animator.animatedValue as Float)
+ val animator = ValueAnimator.ofFloat(1f, 0f).setDuration(ANIMATION_DURATION_MS)
+ when (animationType) {
+ AnimationType.FADE ->
+ animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float }
+ AnimationType.COLLAPSE ->
+ animator.addUpdateListener { _ ->
+ flyout.updateExpansionProgress(animator.animatedValue as Float)
+ }
}
animator.doOnEnd {
container.removeView(flyout)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
index c60fba2..6903c87 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -140,6 +140,7 @@
init {
LayoutInflater.from(context).inflate(R.layout.bubblebar_flyout, this, true)
+ id = R.id.bubble_bar_flyout_view
val ta = context.obtainStyledAttributes(intArrayOf(android.R.attr.dialogCornerRadius))
cornerRadius = ta.getDimensionPixelSize(0, 0).toFloat()
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index e23947b..461f963 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -53,7 +53,6 @@
import com.android.systemui.shared.system.InteractionJankMonitorWrapper
import java.io.PrintWriter
import java.util.concurrent.ConcurrentLinkedDeque
-import java.util.concurrent.Executor
import kotlin.coroutines.resume
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@@ -70,7 +69,6 @@
private val overviewComponentObserver: OverviewComponentObserver,
private val taskAnimationManager: TaskAnimationManager,
private val dispatcherProvider: DispatcherProvider = ProductionDispatchers,
- private val uiExecutor: Executor = Executors.MAIN_EXECUTOR,
) {
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.default)
@@ -87,7 +85,7 @@
get() = overviewComponentObserver.containerInterface
private val visibleRecentsView: RecentsView<*, *>?
- get() = containerInterface.getVisibleRecentsView()
+ get() = containerInterface.getVisibleRecentsView<RecentsView<*, *>>()
/**
* Adds a command to be executed next, after all pending tasks are completed. Max commands that
@@ -107,7 +105,11 @@
if (commandQueue.size == 1) {
Log.d(TAG, "execute: $command - queue size: ${commandQueue.size}")
- uiExecutor.execute { processNextCommand() }
+ if (enableOverviewCommandHelperTimeout()) {
+ coroutineScope.launch(dispatcherProvider.main) { processNextCommand() }
+ } else {
+ Executors.MAIN_EXECUTOR.execute { processNextCommand() }
+ }
} else {
Log.d(TAG, "not executed: $command - queue size: ${commandQueue.size}")
}
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 91fa72d..c4221a1 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -116,6 +116,10 @@
() -> getCacheEntry(task),
MAIN_EXECUTOR,
result -> {
+ task.icon = result.icon;
+ task.titleDescription = result.contentDescription;
+ task.title = result.title;
+
callback.onTaskIconReceived(
result.icon,
result.contentDescription,
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
index 9c4248c..3b59864 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
@@ -40,5 +40,5 @@
* Sets the tasks that are visible, indicating that properties relating to visuals need to be
* populated e.g. icons/thumbnails etc.
*/
- fun setVisibleTasks(visibleTaskIdList: List<Int>)
+ fun setVisibleTasks(visibleTaskIdList: Set<Int>)
}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index dc8d537..4f38ec7 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -23,147 +23,147 @@
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
-import com.android.quickstep.util.GroupTask
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import kotlin.coroutines.resume
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
-@OptIn(ExperimentalCoroutinesApi::class)
class TasksRepository(
private val recentsModel: RecentTasksDataSource,
private val taskThumbnailDataSource: TaskThumbnailDataSource,
private val taskIconDataSource: TaskIconDataSource,
private val taskVisualsChangedDelegate: TaskVisualsChangedDelegate,
- recentsCoroutineScope: CoroutineScope,
+ private val recentsCoroutineScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
) : RecentTasksRepository {
- private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
- private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
-
- private val taskData =
- groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } }
- private val visibleTasks =
- combine(taskData, visibleTaskIds) { tasks, visibleIds ->
- tasks.filter { it.key.id in visibleIds }
- }
-
- private val iconQueryResults: Flow<Map<Int, TaskIconQueryResponse?>> =
- visibleTasks
- .map { visibleTasksList -> visibleTasksList.map(::getIconDataRequest) }
- .flatMapLatest { iconRequestFlows: List<IconDataRequest> ->
- if (iconRequestFlows.isEmpty()) {
- flowOf(emptyMap())
- } else {
- combine(iconRequestFlows) { it.toMap() }
- }
- }
- .distinctUntilChanged()
-
- private val thumbnailQueryResults: Flow<Map<Int, ThumbnailData?>> =
- visibleTasks
- .map { visibleTasksList -> visibleTasksList.map(::getThumbnailDataRequest) }
- .flatMapLatest { thumbnailRequestFlows: List<ThumbnailDataRequest> ->
- if (thumbnailRequestFlows.isEmpty()) {
- flowOf(emptyMap())
- } else {
- combine(thumbnailRequestFlows) { it.toMap() }
- }
- }
- .distinctUntilChanged()
-
- private val augmentedTaskData: Flow<List<Task>> =
- combine(taskData, thumbnailQueryResults, iconQueryResults) {
- tasks,
- thumbnailQueryResults,
- iconQueryResults ->
- tasks.onEach { task ->
- // Add retrieved thumbnails + remove unnecessary thumbnails (e.g. invisible)
- task.thumbnail = thumbnailQueryResults[task.key.id]
-
- // TODO(b/352331675) don't load icons for DesktopTaskView
- // Add retrieved icons + remove unnecessary icons
- val iconQueryResult = iconQueryResults[task.key.id]
- task.icon = iconQueryResult?.icon
- task.titleDescription = iconQueryResult?.contentDescription
- task.title = iconQueryResult?.title
- }
- }
- .flowOn(dispatcherProvider.io)
- .shareIn(recentsCoroutineScope, SharingStarted.WhileSubscribed(5000), replay = 1)
+ private val tasks = MutableStateFlow(MapForStateFlow<Int, Task>(emptyMap()))
+ private val taskRequests = HashMap<Int, Pair<Task.TaskKey, Job>>()
override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> {
if (forceRefresh) {
- recentsModel.getTasks { groupedTaskData.value = it }
+ recentsModel.getTasks { result ->
+ tasks.value =
+ MapForStateFlow(
+ result
+ .flatMap { groupTask -> groupTask.tasks }
+ .associateBy { it.key.id }
+ .also {
+ // Clean tasks that are not in the latest group tasks list.
+ val tasksNoLongerVisible = it.keys.subtract(tasks.value.keys)
+ removeTasks(tasksNoLongerVisible)
+ }
+ )
+ }
}
- return augmentedTaskData
+ return tasks.map { it.values.toList() }
}
- override fun getTaskDataById(taskId: Int): Flow<Task?> =
- augmentedTaskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
+ override fun getTaskDataById(taskId: Int) = tasks.map { it[taskId] }
- override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
+ override fun getThumbnailById(taskId: Int) =
getTaskDataById(taskId).map { it?.thumbnail }.distinctUntilChangedBy { it?.snapshotId }
- override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
+ override fun setVisibleTasks(visibleTaskIdList: Set<Int>) {
Log.d(TAG, "setVisibleTasks: $visibleTaskIdList")
- this.visibleTaskIds.value = visibleTaskIdList.toSet()
+
+ // Remove tasks are no longer visible
+ val tasksNoLongerVisible = taskRequests.keys.subtract(visibleTaskIdList)
+ removeTasks(tasksNoLongerVisible)
+ // Add new tasks to be requested
+ visibleTaskIdList.subtract(taskRequests.keys).forEach { taskId -> requestTaskData(taskId) }
}
- /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
- private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = callbackFlow {
- trySend(task.key.id to task.thumbnail)
- trySend(task.key.id to getThumbnailFromDataSource(task))
+ private fun requestTaskData(taskId: Int) {
+ Log.i(TAG, "requestTaskData: $taskId")
+ val task = tasks.value[taskId] ?: return
+ taskRequests[taskId] =
+ Pair(
+ task.key,
+ recentsCoroutineScope.launch {
+ fetchIcon(task)
+ fetchThumbnail(task)
+ },
+ )
+ }
- val callback =
+ private fun removeTasks(tasksToRemove: Set<Int>) {
+ if (tasksToRemove.isEmpty()) return
+
+ tasksToRemove.forEach { taskId ->
+ Log.i(TAG, "removeTask: $taskId")
+ val request = taskRequests.remove(taskId) ?: return
+ val (taskKey, job) = request
+ job.cancel()
+
+ // un-registering callbacks
+ taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(taskKey)
+ taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(taskKey)
+
+ // Clearing Task to reduce memory footprint
+ tasks.value[taskId]?.apply {
+ thumbnail = null
+ icon = null
+ title = null
+ titleDescription = null
+ }
+ }
+ tasks.update { oldValue -> MapForStateFlow(oldValue) }
+ }
+
+ private suspend fun fetchIcon(task: Task) {
+ updateIcon(task.key.id, getIconFromDataSource(task)) // Fetch icon from cache
+ taskVisualsChangedDelegate.registerTaskIconChangedCallback(
+ task.key,
+ object : TaskIconChangedCallback {
+ override fun onTaskIconChanged() {
+ recentsCoroutineScope.launch {
+ updateIcon(task.key.id, getIconFromDataSource(task))
+ }
+ }
+ },
+ )
+ }
+
+ private suspend fun fetchThumbnail(task: Task) {
+ updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
+ taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(
+ task.key,
object : TaskThumbnailChangedCallback {
override fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) {
- trySend(task.key.id to thumbnailData)
+ updateThumbnail(task.key.id, thumbnailData)
}
override fun onHighResLoadingStateChanged() {
- launch { trySend(task.key.id to getThumbnailFromDataSource(task)) }
+ recentsCoroutineScope.launch {
+ updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
+ }
}
- }
- taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(task.key, callback)
- awaitClose { taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(task.key) }
+ },
+ )
}
- /** Flow wrapper for [TaskIconDataSource.getIconInBackground] api */
- private fun getIconDataRequest(task: Task): IconDataRequest =
- callbackFlow {
- trySend(task.key.id to task.getTaskIconQueryResponse())
- trySend(task.key.id to getIconFromDataSource(task))
+ private fun updateIcon(taskId: Int, iconData: IconData) {
+ val task = tasks.value[taskId] ?: return
+ task.icon = iconData.icon
+ task.titleDescription = iconData.contentDescription
+ task.title = iconData.title
+ tasks.update { oldValue -> MapForStateFlow(oldValue + (taskId to task)) }
+ }
- val callback =
- object : TaskIconChangedCallback {
- override fun onTaskIconChanged() {
- launch { trySend(task.key.id to getIconFromDataSource(task)) }
- }
- }
- taskVisualsChangedDelegate.registerTaskIconChangedCallback(task.key, callback)
- awaitClose {
- taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(task.key)
- }
- }
- .distinctUntilChanged()
+ private fun updateThumbnail(taskId: Int, thumbnail: ThumbnailData?) {
+ val task = tasks.value[taskId] ?: return
+ task.thumbnail = thumbnail
+ tasks.update { oldValue -> MapForStateFlow(oldValue + (taskId to task)) }
+ }
private suspend fun getThumbnailFromDataSource(task: Task) =
withContext(dispatcherProvider.main) {
@@ -184,11 +184,7 @@
->
icon.constantState?.let {
continuation.resume(
- TaskIconQueryResponse(
- it.newDrawable().mutate(),
- contentDescription,
- title,
- )
+ IconData(it.newDrawable().mutate(), contentDescription, title)
)
}
}
@@ -199,22 +195,16 @@
companion object {
private const val TAG = "TasksRepository"
}
+
+ /** Helper class to support StateFlow emissions when using a Map with a MutableStateFlow. */
+ private data class MapForStateFlow<K, T>(
+ private val backingMap: Map<K, T>,
+ private val updated: Long = System.nanoTime(),
+ ) : Map<K, T> by backingMap
+
+ private data class IconData(
+ val icon: Drawable,
+ val contentDescription: String,
+ val title: String,
+ )
}
-
-data class TaskIconQueryResponse(
- val icon: Drawable,
- val contentDescription: String,
- val title: String,
-)
-
-private fun Task.getTaskIconQueryResponse(): TaskIconQueryResponse? {
- val iconVal = icon ?: return null
- val titleDescriptionVal = titleDescription ?: return null
- val titleVal = title ?: return null
-
- return TaskIconQueryResponse(iconVal, titleDescriptionVal, titleVal)
-}
-
-private typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>
-
-private typealias IconDataRequest = Flow<Pair<Int, TaskIconQueryResponse?>>
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index 5cf6823..c511005 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -31,7 +31,7 @@
}
fun updateVisibleTasks(visibleTaskIdList: List<Int>) {
- recentsTasksRepository.setVisibleTasks(visibleTaskIdList)
+ recentsTasksRepository.setVisibleTasks(visibleTaskIdList.toSet())
}
fun updateScale(scale: Float) {
diff --git a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
index 4a84b1b..54f6443 100644
--- a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
+++ b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
@@ -34,11 +34,8 @@
import com.android.launcher3.dagger.ApplicationContext;
import com.android.launcher3.dagger.LauncherAppSingleton;
-import com.android.launcher3.dagger.LauncherBaseAppComponent;
import com.android.launcher3.util.DaggerSingletonObject;
import com.android.launcher3.util.DaggerSingletonTracker;
-import com.android.launcher3.util.ExecutorUtil;
-import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.SettingsCache;
import com.android.launcher3.util.SettingsCache.OnChangeListener;
@@ -61,6 +58,7 @@
new DaggerSingletonObject<>(QuickstepBaseAppComponent::getAsyncClockEventDelegate);
private final Context mContext;
+ private final SettingsCache mSettingsCache;
private final SimpleBroadcastReceiver mReceiver =
new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onClockEventReceived);
@@ -72,11 +70,14 @@
private boolean mDestroyed = false;
@Inject
- AsyncClockEventDelegate(@ApplicationContext Context context, DaggerSingletonTracker tracker) {
+ AsyncClockEventDelegate(@ApplicationContext Context context,
+ DaggerSingletonTracker tracker,
+ SettingsCache settingsCache) {
super(context);
mContext = context;
+ mSettingsCache = settingsCache;
mReceiver.register(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED);
- ExecutorUtil.executeSyncOnMainOrFail(() -> tracker.addCloseable(this));
+ tracker.addCloseable(this);
}
@Override
@@ -100,7 +101,7 @@
}
synchronized (mFormatObservers) {
if (!mFormatRegistered && !mDestroyed) {
- SettingsCache.INSTANCE.get(mContext).register(mFormatUri, this);
+ mSettingsCache.register(mFormatUri, this);
mFormatRegistered = true;
}
mFormatObservers.add(observer);
@@ -136,7 +137,7 @@
@Override
public void close() {
mDestroyed = true;
- SettingsCache.INSTANCE.get(mContext).unregister(mFormatUri, this);
+ mSettingsCache.unregister(mFormatUri, this);
mReceiver.unregisterReceiverSafely(mContext);
}
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 7b67780..b38d0d7 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -3951,11 +3951,9 @@
if (shouldRemoveTask) {
if (dismissedTaskView.isRunningTask()) {
finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
- () -> removeTaskInternal(dismissedTaskViewId,
- dismissedTaskView instanceof DesktopTaskView));
+ () -> removeTaskInternal(dismissedTaskView));
} else {
- removeTaskInternal(dismissedTaskViewId,
- dismissedTaskView instanceof DesktopTaskView);
+ removeTaskInternal(dismissedTaskView);
}
announceForAccessibility(
getResources().getString(R.string.task_view_closed));
@@ -4323,22 +4321,24 @@
return lastVisibleIndex;
}
- private void removeTaskInternal(int dismissedTaskViewId, boolean isDesktop) {
- int[] taskIds = getTaskIdsForTaskViewId(dismissedTaskViewId);
- UI_HELPER_EXECUTOR.getHandler().post(() -> {
- if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() && isDesktop) {
- // TODO: b/372005228 - Use the api with desktop id instead.
- SystemUiProxy.INSTANCE.get(getContext()).removeDesktop(
- mContainer.getDisplay().getDisplayId());
- } else {
- for (int taskId : taskIds) {
- if (taskId != -1) {
- ActivityManagerWrapper.getInstance().removeTask(taskId);
- }
+ private void removeTaskInternal(@NonNull TaskView dismissedTaskView) {
+ UI_HELPER_EXECUTOR
+ .getHandler()
+ .post(
+ () -> {
+ if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()
+ && dismissedTaskView instanceof DesktopTaskView) {
+ // TODO: b/362720497 - Use the api with desktop id instead.
+ SystemUiProxy.INSTANCE
+ .get(getContext())
+ .removeDesktop(mContainer.getDisplay().getDisplayId());
+ } else {
+ for (int taskId : dismissedTaskView.getTaskIds()) {
+ ActivityManagerWrapper.getInstance().removeTask(taskId);
}
- }
- });
- }
+ }
+ });
+ }
protected void onDismissAnimationEnds() {
AccessibilityManagerCompat.sendTestProtocolEventToTest(getContext(),
@@ -5057,7 +5057,8 @@
mSplitHiddenTaskView = getTaskViewByTaskId(splitSelectSource.alreadyRunningTaskId);
mSplitHiddenTaskViewIndex = indexOfChild(mSplitHiddenTaskView);
mSplitSelectStateController
- .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal);
+ .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal
+ && mSplitHiddenTaskView != null);
// Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair
mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null
@@ -5442,7 +5443,6 @@
int taskIndex = indexOfChild(taskView);
int centerTaskIndex = getCurrentPage();
- boolean isRunningTask = taskView.isRunningTask();
float toScale = getMaxScaleForFullScreen();
boolean showAsGrid = showAsGrid();
@@ -5462,7 +5462,9 @@
setPivotX(mTempPointF.x);
setPivotY(mTempPointF.y);
- if (!isRunningTask) {
+ // If live tile is not launching, apply pivot to live tile as well and bring it
+ // above RecentsView to avoid wallpaper blur from being applied to it.
+ if (!taskView.isRunningTask()) {
runActionOnRemoteHandles(
remoteTargetHandle -> {
remoteTargetHandle.getTaskViewSimulator().setPivotOverride(
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 7eee4de..b37048a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.graphics.Color
import android.graphics.Path
+import android.graphics.PointF
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
@@ -36,6 +37,10 @@
import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow
import com.android.launcher3.taskbar.bubbles.BubbleBarView
import com.android.launcher3.taskbar.bubbles.BubbleView
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner
+import com.android.launcher3.taskbar.bubbles.flyout.FlyoutScheduler
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
import com.android.wm.shell.shared.animation.PhysicsAnimator
import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
@@ -63,13 +68,19 @@
private lateinit var bubbleView: BubbleView
private lateinit var bubble: BubbleBarBubble
private lateinit var bubbleBarView: BubbleBarView
+ private lateinit var flyoutContainer: FrameLayout
private lateinit var bubbleStashController: BubbleStashController
+ private lateinit var flyoutController: BubbleBarFlyoutController
private val onExpandedNoOp = Runnable {}
+ private val flyoutView: View?
+ get() = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view)
+
@Before
fun setUp() {
animatorScheduler = TestBubbleBarViewAnimatorScheduler()
PhysicsAnimatorTestUtils.prepareForTest()
+ setupFlyoutController()
}
@Test
@@ -85,6 +96,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpandedNoOp,
animatorScheduler,
)
@@ -106,10 +118,14 @@
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(animator.isAnimating).isTrue()
+ waitForFlyoutToShow()
+
// execute the hide bubble animation
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+ waitForFlyoutToHide()
+
// let the animation start and wait for it to complete
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
@@ -134,6 +150,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpandedNoOp,
animatorScheduler,
)
@@ -157,10 +174,16 @@
verify(bubbleStashController, atLeastOnce()).updateTaskbarTouchRegion()
+ waitForFlyoutToShow()
+
// verify the hide bubble animation is pending
assertThat(animatorScheduler.delayedBlock).isNotNull()
- animator.onBubbleBarTouchedWhileAnimating()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.onBubbleBarTouchedWhileAnimating()
+ }
+
+ waitForFlyoutToHide()
assertThat(animatorScheduler.delayedBlock).isNull()
assertThat(bubbleBarView.alpha).isEqualTo(1)
@@ -182,6 +205,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpandedNoOp,
animatorScheduler,
)
@@ -227,6 +251,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpandedNoOp,
animatorScheduler,
)
@@ -239,10 +264,14 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+ waitForFlyoutToShow()
+
// execute the hide bubble animation
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+ waitForFlyoutToHide()
+
// wait for the hide animation to start
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
handleAnimator.assertIsRunning()
@@ -273,6 +302,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpandedNoOp,
animatorScheduler,
)
@@ -310,6 +340,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpanded,
animatorScheduler,
)
@@ -354,6 +385,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpanded,
animatorScheduler,
)
@@ -404,6 +436,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpanded,
animatorScheduler,
)
@@ -418,6 +451,9 @@
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
assertThat(animator.isAnimating).isTrue()
+
+ waitForFlyoutToShow()
+
// verify the hide bubble animation is pending
assertThat(animatorScheduler.delayedBlock).isNotNull()
@@ -428,6 +464,8 @@
// verify that the hide animation was canceled
assertThat(animatorScheduler.delayedBlock).isNull()
+ waitForFlyoutToHide()
+
assertThat(handle.alpha).isEqualTo(0)
assertThat(handle.translationY)
.isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
@@ -453,6 +491,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpandedNoOp,
animatorScheduler,
)
@@ -469,9 +508,13 @@
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ waitForFlyoutToShow()
+
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+ waitForFlyoutToHide()
+
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
@@ -503,6 +546,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpanded,
animatorScheduler,
)
@@ -537,6 +581,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpandedNoOp,
animatorScheduler,
)
@@ -553,9 +598,13 @@
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ waitForFlyoutToShow()
+
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+ waitForFlyoutToHide()
+
assertThat(animator.isAnimating).isFalse()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
@@ -576,6 +625,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpanded,
animatorScheduler,
)
@@ -624,6 +674,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpanded,
animatorScheduler,
)
@@ -636,6 +687,8 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+ waitForFlyoutToShow()
+
assertThat(animator.isAnimating).isTrue()
// verify the hide bubble animation is pending
assertThat(animatorScheduler.delayedBlock).isNotNull()
@@ -644,6 +697,8 @@
animator.expandedWhileAnimating()
}
+ waitForFlyoutToHide()
+
// verify that the hide animation was canceled
assertThat(animatorScheduler.delayedBlock).isNull()
@@ -665,6 +720,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpandedNoOp,
animatorScheduler,
)
@@ -687,9 +743,13 @@
barAnimator.assertIsRunning()
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+ waitForFlyoutToShow()
+
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+ waitForFlyoutToHide()
+
assertThat(animator.isAnimating).isFalse()
// the bubble bar translation y should be back to its initial value
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
@@ -712,6 +772,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpanded,
animatorScheduler,
)
@@ -759,6 +820,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpanded,
animatorScheduler,
)
@@ -817,6 +879,7 @@
BubbleBarViewAnimator(
bubbleBarView,
bubbleStashController,
+ flyoutController,
onExpanded,
animatorScheduler,
)
@@ -843,6 +906,8 @@
assertThat(animatorScheduler.delayedBlock).isNotNull()
assertThat(animator.isAnimating).isTrue()
+ waitForFlyoutToShow()
+
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.expandedWhileAnimating()
}
@@ -850,6 +915,8 @@
// verify that the hide animation was canceled
assertThat(animatorScheduler.delayedBlock).isNull()
+ waitForFlyoutToHide()
+
assertThat(animator.isAnimating).isFalse()
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
assertThat(bubbleBarView.isExpanded).isTrue()
@@ -894,7 +961,7 @@
Color.WHITE,
Path(),
"",
- null,
+ BubbleBarFlyoutMessage(icon = null, title = "title", message = "message"),
)
bubbleView.setBubble(bubble)
bubbleBarView.addView(bubbleView)
@@ -913,6 +980,34 @@
.thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
}
+ private fun setupFlyoutController() {
+ flyoutContainer = FrameLayout(context)
+ val flyoutPositioner =
+ object : BubbleBarFlyoutPositioner {
+ override val isOnLeft = true
+ override val targetTy = 100f
+ override val distanceToCollapsedPosition = PointF(0f, 0f)
+ override val collapsedSize = 30f
+ override val collapsedColor = Color.BLUE
+ override val collapsedElevation = 1f
+ override val distanceToRevealTriangle = 10f
+ }
+ val topBoundaryListener =
+ object : BubbleBarFlyoutController.TopBoundaryListener {
+ override fun extendTopBoundary(space: Int) {}
+
+ override fun resetTopBoundary() {}
+ }
+ val flyoutScheduler = FlyoutScheduler { block -> block.invoke() }
+ flyoutController =
+ BubbleBarFlyoutController(
+ flyoutContainer,
+ flyoutPositioner,
+ topBoundaryListener,
+ flyoutScheduler,
+ )
+ }
+
private fun verifyBubbleBarIsExpandedWithTranslation(ty: Float) {
assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
assertThat(bubbleBarView.scaleX).isEqualTo(1)
@@ -921,6 +1016,20 @@
assertThat(bubbleBarView.isExpanded).isTrue()
}
+ private fun waitForFlyoutToShow() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(300)
+ }
+ assertThat(flyoutView).isNotNull()
+ }
+
+ private fun waitForFlyoutToHide() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(300)
+ }
+ assertThat(flyoutView).isNull()
+ }
+
private fun <T> PhysicsAnimator<T>.assertIsRunning() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
assertThat(isRunning()).isTrue()
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
index 3dd7689..527bdaa 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
@@ -20,6 +20,7 @@
import android.graphics.Color
import android.graphics.PointF
import android.view.Gravity
+import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.animation.AnimatorTestRule
@@ -80,7 +81,7 @@
@Test
fun flyoutPosition_left() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpFlyout(flyoutMessage)
+ flyoutController.setUpAndShowFlyout(flyoutMessage) {}
assertThat(flyoutContainer.childCount).isEqualTo(1)
val flyout = flyoutContainer.getChildAt(0)
val lp = flyout.layoutParams as FrameLayout.LayoutParams
@@ -93,7 +94,7 @@
fun flyoutPosition_right() {
onLeft = false
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpFlyout(flyoutMessage)
+ flyoutController.setUpAndShowFlyout(flyoutMessage) {}
assertThat(flyoutContainer.childCount).isEqualTo(1)
val flyout = flyoutContainer.getChildAt(0)
val lp = flyout.layoutParams as FrameLayout.LayoutParams
@@ -105,7 +106,7 @@
@Test
fun flyoutMessage() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpFlyout(flyoutMessage)
+ flyoutController.setUpAndShowFlyout(flyoutMessage) {}
assertThat(flyoutContainer.childCount).isEqualTo(1)
val flyout = flyoutContainer.getChildAt(0)
val sender = flyout.findViewById<TextView>(R.id.bubble_flyout_title)
@@ -118,9 +119,9 @@
@Test
fun hideFlyout_removedFromContainer() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpFlyout(flyoutMessage)
+ flyoutController.setUpAndShowFlyout(flyoutMessage) {}
assertThat(flyoutContainer.childCount).isEqualTo(1)
- flyoutController.hideFlyout {}
+ flyoutController.collapseFlyout {}
animatorTestRule.advanceTimeBy(300)
}
assertThat(flyoutContainer.childCount).isEqualTo(0)
@@ -132,7 +133,7 @@
// boundary
flyoutTy = -50f
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpFlyout(flyoutMessage)
+ flyoutController.setUpAndShowFlyout(flyoutMessage) {}
assertThat(flyoutContainer.childCount).isEqualTo(1)
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
@@ -145,7 +146,7 @@
@Test
fun showFlyout_withinBoundary() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpFlyout(flyoutMessage)
+ flyoutController.setUpAndShowFlyout(flyoutMessage) {}
assertThat(flyoutContainer.childCount).isEqualTo(1)
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
@@ -156,16 +157,30 @@
}
@Test
- fun hideFlyout_resetsTopBoundary() {
+ fun collapseFlyout_resetsTopBoundary() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpFlyout(flyoutMessage)
+ flyoutController.setUpAndShowFlyout(flyoutMessage) {}
assertThat(flyoutContainer.childCount).isEqualTo(1)
- flyoutController.hideFlyout {}
+ flyoutController.collapseFlyout {}
animatorTestRule.advanceTimeBy(300)
}
assertThat(topBoundaryListener.topBoundaryReset).isTrue()
}
+ @Test
+ fun cancelFlyout_fadesOutFlyout() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ assertThat(flyoutContainer.childCount).isEqualTo(1)
+ val flyoutView = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+ assertThat(flyoutView.alpha).isEqualTo(1f)
+ flyoutController.cancelFlyout {}
+ animatorTestRule.advanceTimeBy(300)
+ assertThat(flyoutView.alpha).isEqualTo(0f)
+ }
+ assertThat(topBoundaryListener.topBoundaryReset).isTrue()
+ }
+
class FakeTopBoundaryListener : BubbleBarFlyoutController.TopBoundaryListener {
var topBoundaryExtendedSpace = 0
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
index b0db737..0ae710f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
@@ -41,6 +41,7 @@
import org.junit.runner.RunWith
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when`
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
@@ -55,12 +56,10 @@
private val testScope = TestScope(dispatcher)
private var pendingCallbacksWithDelays = mutableListOf<Long>()
- private lateinit var pendingCommandsToExecute: MutableList<Runnable>
@Suppress("UNCHECKED_CAST")
@Before
fun setup() {
- pendingCommandsToExecute = mutableListOf()
setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT)
sut =
@@ -69,8 +68,7 @@
touchInteractionService = mock(),
overviewComponentObserver = mock(),
taskAnimationManager = mock(),
- dispatcherProvider = TestDispatcherProvider(dispatcher),
- uiExecutor = { runnable -> pendingCommandsToExecute += runnable },
+ dispatcherProvider = TestDispatcherProvider(dispatcher)
)
)
@@ -96,21 +94,12 @@
pendingCallbacksWithDelays.add(delayInMillis)
}
- /**
- * This function runs all the pending commands from the Executor for testing purposes. Replacing
- * the uiExecutor allows the test to execute the command queue manually, making it possible to
- * assert each state of the commands in the queue individually.
- */
- private fun executePendingCommands() = pendingCommandsToExecute.forEach { it.run() }
-
@Test
fun whenFirstCommandIsAdded_executeCommandImmediately() =
testScope.runTest {
// Add command to queue
val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME)!!
assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE)
- executePendingCommands()
- assertThat(commandInfo.status).isEqualTo(CommandStatus.PROCESSING)
runCurrent()
assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED)
}
@@ -125,7 +114,7 @@
val commandInfo: CommandInfo = sut.addCommand(commandType)!!
assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE)
- executePendingCommands()
+ runCurrent()
assertThat(commandInfo.status).isEqualTo(CommandStatus.PROCESSING)
advanceTimeBy(200L)
@@ -146,14 +135,12 @@
val commandInfo2: CommandInfo = sut.addCommand(commandType2)!!
assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
- executePendingCommands()
+ runCurrent()
assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING)
assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
advanceTimeBy(101L)
assertThat(commandInfo1.status).isEqualTo(CommandStatus.COMPLETED)
-
- executePendingCommands()
assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING)
advanceTimeBy(101L)
@@ -174,14 +161,12 @@
val commandInfo2: CommandInfo = sut.addCommand(commandType2)!!
assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
- executePendingCommands()
+ runCurrent()
assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING)
assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
advanceTimeBy(QUEUE_TIMEOUT)
assertThat(commandInfo1.status).isEqualTo(CommandStatus.CANCELED)
-
- executePendingCommands()
assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING)
advanceTimeBy(101)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
index 7a17872..d6688d6 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -16,6 +16,7 @@
package com.android.quickstep.recents.data
+import android.graphics.drawable.Drawable
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import kotlinx.coroutines.flow.Flow
@@ -25,9 +26,9 @@
class FakeTasksRepository : RecentTasksRepository {
private var thumbnailDataMap: Map<Int, ThumbnailData> = emptyMap()
- private var taskIconDataMap: Map<Int, TaskIconQueryResponse> = emptyMap()
+ private var taskIconDataMap: Map<Int, FakeIconData> = emptyMap()
private var tasks: MutableStateFlow<List<Task>> = MutableStateFlow(emptyList())
- private var visibleTasks: MutableStateFlow<List<Int>> = MutableStateFlow(emptyList())
+ private var visibleTasks: MutableStateFlow<Set<Int>> = MutableStateFlow(emptySet())
override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> = tasks
@@ -48,16 +49,16 @@
override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
getTaskDataById(taskId).map { it?.thumbnail }
- override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
+ override fun setVisibleTasks(visibleTaskIdList: Set<Int>) {
visibleTasks.value = visibleTaskIdList
tasks.value =
tasks.value.map {
it.apply {
thumbnail = thumbnailDataMap[it.key.id]
- taskIconDataMap[it.key.id].let { taskIconData ->
- icon = taskIconData?.icon
- titleDescription = taskIconData?.contentDescription
- title = taskIconData?.title
+ taskIconDataMap[it.key.id].let { data ->
+ title = data?.title
+ titleDescription = data?.titleDescription
+ icon = data?.icon
}
}
}
@@ -71,7 +72,14 @@
this.thumbnailDataMap = thumbnailDataMap
}
- fun seedIconData(iconDataMap: Map<Int, TaskIconQueryResponse>) {
- this.taskIconDataMap = iconDataMap
+ fun seedIconData(id: Int, title: String, contentDescription: String, icon: Drawable) {
+ val iconData = FakeIconData(icon, contentDescription, title)
+ this.taskIconDataMap = mapOf(id to iconData)
}
+
+ private data class FakeIconData(
+ val icon: Drawable,
+ val titleDescription: String,
+ val title: String,
+ )
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
index d55f2e3..357df6e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
@@ -74,7 +74,6 @@
fun getAllTaskDataReturnsFlattenedListOfTasks() =
testScope.runTest {
recentsModel.seedTasks(defaultTaskList)
-
assertThat(systemUnderTest.getAllTaskData(forceRefresh = true).first()).isEqualTo(tasks)
}
@@ -95,7 +94,7 @@
val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
systemUnderTest.getAllTaskData(forceRefresh = true)
- systemUnderTest.setVisibleTasks(listOf(1, 2))
+ systemUnderTest.setVisibleTasks(setOf(1, 2))
assertThat(systemUnderTest.getTaskDataById(1).first()!!.thumbnail!!.thumbnail)
.isEqualTo(bitmap1)
@@ -109,7 +108,7 @@
recentsModel.seedTasks(defaultTaskList)
systemUnderTest.getAllTaskData(forceRefresh = true)
- systemUnderTest.setVisibleTasks(listOf(1, 2))
+ systemUnderTest.setVisibleTasks(setOf(1, 2))
systemUnderTest
.getTaskDataById(1)
@@ -128,14 +127,14 @@
val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
systemUnderTest.getAllTaskData(forceRefresh = true)
- systemUnderTest.setVisibleTasks(listOf(1, 2))
+ systemUnderTest.setVisibleTasks(setOf(1, 2))
assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
.isEqualTo(bitmap2)
// Prevent new loading of Bitmaps
taskThumbnailDataSource.shouldLoadSynchronously = false
- systemUnderTest.setVisibleTasks(listOf(2, 3))
+ systemUnderTest.setVisibleTasks(setOf(2, 3))
assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
.isEqualTo(bitmap2)
@@ -147,7 +146,7 @@
recentsModel.seedTasks(defaultTaskList)
systemUnderTest.getAllTaskData(forceRefresh = true)
- systemUnderTest.setVisibleTasks(listOf(1, 2))
+ systemUnderTest.setVisibleTasks(setOf(1, 2))
systemUnderTest
.getTaskDataById(2)
@@ -156,7 +155,7 @@
// Prevent new loading of Drawables
taskThumbnailDataSource.shouldLoadSynchronously = false
- systemUnderTest.setVisibleTasks(listOf(2, 3))
+ systemUnderTest.setVisibleTasks(setOf(2, 3))
systemUnderTest
.getTaskDataById(2)
@@ -171,7 +170,7 @@
val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
systemUnderTest.getAllTaskData(forceRefresh = true)
- systemUnderTest.setVisibleTasks(listOf(1, 2))
+ systemUnderTest.setVisibleTasks(setOf(1, 2))
val task2 = systemUnderTest.getTaskDataById(2).first()!!
assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2)
@@ -180,7 +179,7 @@
// Prevent new loading of Bitmaps
taskThumbnailDataSource.shouldLoadSynchronously = false
taskIconDataSource.shouldLoadSynchronously = false
- systemUnderTest.setVisibleTasks(listOf(0, 1))
+ systemUnderTest.setVisibleTasks(setOf(0, 1))
val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!!
assertThat(task2AfterVisibleTasksChanged.thumbnail).isNull()
@@ -199,7 +198,7 @@
// Setup TasksRepository
systemUnderTest.getAllTaskData(forceRefresh = true)
- systemUnderTest.setVisibleTasks(listOf(1, 2))
+ systemUnderTest.setVisibleTasks(setOf(1, 2))
// Assert there is no bitmap in first emission
assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull()
@@ -217,8 +216,7 @@
testScope.runTest {
recentsModel.seedTasks(defaultTaskList)
systemUnderTest.getAllTaskData(forceRefresh = true)
-
- systemUnderTest.setVisibleTasks(listOf(1))
+ systemUnderTest.setVisibleTasks(setOf(1))
val expectedThumbnailData = createThumbnailData()
val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
@@ -230,7 +228,7 @@
}
taskVisualsChangedDelegate.onTaskThumbnailChanged(1, expectedThumbnailData)
- assertThat(task1ThumbnailValues[1]!!.thumbnail).isEqualTo(expectedPreviousBitmap)
+ assertThat(task1ThumbnailValues.first()!!.thumbnail).isEqualTo(expectedPreviousBitmap)
assertThat(task1ThumbnailValues.last()).isEqualTo(expectedThumbnailData)
}
@@ -240,7 +238,7 @@
recentsModel.seedTasks(defaultTaskList)
systemUnderTest.getAllTaskData(forceRefresh = true)
- systemUnderTest.setVisibleTasks(listOf(1))
+ systemUnderTest.setVisibleTasks(setOf(1))
val expectedBitmap = mock<Bitmap>()
val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
@@ -250,10 +248,11 @@
testScope.backgroundScope.launch {
taskDataFlow.map { it?.thumbnail?.thumbnail }.toList(task1ThumbnailValues)
}
+
taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
- assertThat(task1ThumbnailValues[1]).isEqualTo(expectedPreviousBitmap)
+ assertThat(task1ThumbnailValues.first()).isEqualTo(expectedPreviousBitmap)
assertThat(task1ThumbnailValues.last()).isEqualTo(expectedBitmap)
}
@@ -263,7 +262,7 @@
recentsModel.seedTasks(defaultTaskList)
systemUnderTest.getAllTaskData(forceRefresh = true)
- systemUnderTest.setVisibleTasks(listOf(1))
+ systemUnderTest.setVisibleTasks(setOf(1))
val expectedIcon = FakeTaskIconDataSource.mockCopyableDrawable()
val expectedPreviousIcon = taskIconDataSource.taskIdToDrawable[1]
@@ -276,10 +275,34 @@
taskIconDataSource.taskIdToDrawable[1] = expectedIcon
taskVisualsChangedDelegate.onTaskIconChanged(1)
- assertThat(task1IconValues[1]).isEqualTo(expectedPreviousIcon)
+ assertThat(task1IconValues.first()).isEqualTo(expectedPreviousIcon)
assertThat(task1IconValues.last()).isEqualTo(expectedIcon)
}
+ @Test
+ fun setVisibleTasks_multipleTimesWithDifferentTasks_reusesThumbnailRequests() =
+ testScope.runTest {
+ recentsModel.seedTasks(defaultTaskList)
+ systemUnderTest.getAllTaskData(forceRefresh = true)
+ taskThumbnailDataSource.shouldLoadSynchronously = false
+
+ val taskDataFlow = systemUnderTest.getTaskDataById(1)
+ val task1IconValues = mutableListOf<Drawable?>()
+ testScope.backgroundScope.launch {
+ taskDataFlow.map { it?.icon }.toList(task1IconValues)
+ }
+
+ systemUnderTest.setVisibleTasks(setOf(1))
+ val task1UpdatingTaskOld = taskThumbnailDataSource.taskIdToUpdatingTask[1]
+ println(task1UpdatingTaskOld)
+
+ systemUnderTest.setVisibleTasks(setOf(1, 2))
+ val task1UpdatingTaskNew = taskThumbnailDataSource.taskIdToUpdatingTask[1]
+ println(task1UpdatingTaskNew)
+
+ assertThat(task1UpdatingTaskNew).isEqualTo(task1UpdatingTaskOld)
+ }
+
private fun createTaskWithId(taskId: Int) =
Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000))
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
index 02f1d11..bd7d970 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
@@ -66,7 +66,7 @@
deviceProfileRepository,
rotationStateRepository,
tasksRepository,
- previewPositionHelper
+ previewPositionHelper,
)
@Test
@@ -80,7 +80,7 @@
@Test
fun visibleTaskWithoutThumbnailData_returnsIdentityMatrix() = runTest {
tasksRepository.seedTasks(listOf(task))
- tasksRepository.setVisibleTasks(listOf(TASK_ID))
+ tasksRepository.setVisibleTasks(setOf(TASK_ID))
assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
.isInstanceOf(MissingThumbnail::class.java)
@@ -90,7 +90,7 @@
fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest {
tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
tasksRepository.seedTasks(listOf(task))
- tasksRepository.setVisibleTasks(listOf(TASK_ID))
+ tasksRepository.setVisibleTasks(setOf(TASK_ID))
val isLargeScreen = true
deviceProfileRepository.setRecentsDeviceProfile(
@@ -119,7 +119,7 @@
CANVAS_HEIGHT,
isLargeScreen,
activityRotation,
- isRtl
+ isRtl,
)
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
index 12a94cf..73aa460 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
@@ -71,7 +71,7 @@
fun taskVisible_returnsThumbnail() {
tasksRepository.seedTasks(listOf(task))
tasksRepository.seedThumbnailData(mapOf(TaskOverlayViewModelTest.TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(listOf(TaskOverlayViewModelTest.TASK_ID))
+ tasksRepository.setVisibleTasks(setOf(TaskOverlayViewModelTest.TASK_ID))
assertThat(systemUnderTest.run(TASK_ID)).isEqualTo(thumbnailData.thumbnail)
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
index ba4e206..92f2efd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
@@ -71,7 +71,7 @@
whenever(width).thenReturn(THUMBNAIL_WIDTH)
whenever(height).thenReturn(THUMBNAIL_HEIGHT)
},
- appearance = APPEARANCE_LIGHT_THEME
+ appearance = APPEARANCE_LIGHT_THEME,
)
val secondTask =
@@ -85,14 +85,14 @@
whenever(width).thenReturn(THUMBNAIL_WIDTH)
whenever(height).thenReturn(THUMBNAIL_HEIGHT)
},
- appearance = APPEARANCE_DARK_THEME
+ appearance = APPEARANCE_DARK_THEME,
)
tasksRepository.seedTasks(listOf(firstTask, secondTask))
tasksRepository.seedThumbnailData(
mapOf(FIRST_TASK_ID to firstThumbnailData, SECOND_TASK_ID to secondThumbnailData)
)
- tasksRepository.setVisibleTasks(listOf(FIRST_TASK_ID, SECOND_TASK_ID))
+ tasksRepository.setVisibleTasks(setOf(FIRST_TASK_ID, SECOND_TASK_ID))
}
companion object {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
index 829987c..a32e07d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
@@ -97,7 +97,7 @@
)
// setVisibleTasks forces FakeTasksRepository to update the flows returned by
// getThumbnailById
- tasksRepository.setVisibleTasks(listOf(1, 2))
+ tasksRepository.setVisibleTasks(setOf(1, 2))
// Then wait for thumbnailData should complete, and the previous getThumbnailById flow
// should return updated values
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
index a584d71..0767fb9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
@@ -25,7 +25,6 @@
import android.view.Surface.ROTATION_90
import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.data.TaskIconQueryResponse
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.systemui.shared.recents.model.Task
@@ -49,7 +48,7 @@
taskContainerData,
taskThumbnailViewData,
recentTasksRepository,
- recentsRotationStateRepository
+ recentsRotationStateRepository,
)
@Test
@@ -117,16 +116,16 @@
private fun setupTask(taskId: Int, thumbnailData: ThumbnailData = createThumbnailData()) {
recentTasksRepository.seedThumbnailData(mapOf(taskId to thumbnailData))
- val expectedIconData = createIconData("Task $taskId")
- recentTasksRepository.seedIconData(mapOf(taskId to expectedIconData))
+ val expectedIconData = mock<Drawable>()
+ recentTasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
recentTasksRepository.seedTasks(tasks)
- recentTasksRepository.setVisibleTasks(listOf(taskId))
+ recentTasksRepository.setVisibleTasks(setOf(taskId))
}
private fun createThumbnailData(
rotation: Int = Surface.ROTATION_0,
width: Int = THUMBNAIL_WIDTH,
- height: Int = THUMBNAIL_HEIGHT
+ height: Int = THUMBNAIL_HEIGHT,
): ThumbnailData {
val bitmap = mock<Bitmap>()
whenever(bitmap.width).thenReturn(width)
@@ -135,8 +134,6 @@
return ThumbnailData(thumbnail = bitmap, rotation = rotation)
}
- private fun createIconData(title: String) = TaskIconQueryResponse(mock<Drawable>(), "", title)
-
private fun createTaskWithId(taskId: Int) =
Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
colorBackground = Color.argb(taskId, taskId, taskId, taskId)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
index c88a3fc..c541d3d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
@@ -25,7 +25,6 @@
import android.view.Surface
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.data.TaskIconQueryResponse
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
@@ -81,7 +80,7 @@
fun bindRunningTask_thenStateIs_LiveTile() = runTest {
val taskId = 1
tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(listOf(taskId))
+ tasksRepository.setVisibleTasks(setOf(taskId))
recentsViewData.runningTaskIds.value = setOf(taskId)
systemUnderTest.bind(taskId)
@@ -93,10 +92,10 @@
val taskId = 1
val expectedThumbnailData = createThumbnailData()
tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
- val expectedIconData = createIconData("Task 1")
- tasksRepository.seedIconData(mapOf(taskId to expectedIconData))
+ val expectedIconData = mock<Drawable>()
+ tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(listOf(taskId))
+ tasksRepository.setVisibleTasks(setOf(taskId))
recentsViewData.runningTaskIds.value = setOf(taskId)
recentsViewData.runningTaskShowScreenshot.value = true
systemUnderTest.bind(taskId)
@@ -109,7 +108,7 @@
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_0,
),
- expectedIconData.icon,
+ expectedIconData,
)
)
}
@@ -151,7 +150,7 @@
val runningTaskId = 1
val stoppedTaskId = 2
tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(listOf(runningTaskId, stoppedTaskId))
+ tasksRepository.setVisibleTasks(setOf(runningTaskId, stoppedTaskId))
recentsViewData.runningTaskIds.value = setOf(runningTaskId)
systemUnderTest.bind(runningTaskId)
assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
@@ -165,7 +164,7 @@
fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() = runTest {
val stoppedTaskId = 2
tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(listOf(stoppedTaskId))
+ tasksRepository.setVisibleTasks(setOf(stoppedTaskId))
systemUnderTest.bind(stoppedTaskId)
assertThat(systemUnderTest.uiState.first())
@@ -178,7 +177,7 @@
tasksRepository.seedThumbnailData(mapOf(taskId to createThumbnailData()))
tasks[taskId].isLocked = true
tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(listOf(taskId))
+ tasksRepository.setVisibleTasks(setOf(taskId))
systemUnderTest.bind(taskId)
assertThat(systemUnderTest.uiState.first())
@@ -190,10 +189,10 @@
val taskId = 2
val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
- val expectedIconData = createIconData("Task 2")
- tasksRepository.seedIconData(mapOf(taskId to expectedIconData))
+ val expectedIconData = mock<Drawable>()
+ tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(listOf(taskId))
+ tasksRepository.setVisibleTasks(setOf(taskId))
systemUnderTest.bind(taskId)
assertThat(systemUnderTest.uiState.first())
@@ -204,7 +203,7 @@
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_270,
),
- expectedIconData.icon,
+ expectedIconData,
)
)
}
@@ -214,14 +213,14 @@
val taskId = 2
val expectedThumbnailData = createThumbnailData()
tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
- val expectedIconData = createIconData("Task 2")
- tasksRepository.seedIconData(mapOf(taskId to expectedIconData))
+ val expectedIconData = mock<Drawable>()
+ tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
tasksRepository.seedTasks(tasks)
systemUnderTest.bind(taskId)
assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
- tasksRepository.setVisibleTasks(listOf(taskId))
+ tasksRepository.setVisibleTasks(setOf(taskId))
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
SnapshotSplash(
@@ -230,7 +229,7 @@
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_0,
),
- expectedIconData.icon,
+ expectedIconData,
)
)
}
@@ -295,8 +294,6 @@
return ThumbnailData(thumbnail = bitmap, rotation = rotation)
}
- private fun createIconData(title: String) = TaskIconQueryResponse(mock<Drawable>(), "", title)
-
companion object {
const val THUMBNAIL_WIDTH = 100
const val THUMBNAIL_HEIGHT = 200
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
index d0887df..2e91f5c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
@@ -89,12 +89,7 @@
task.isLocked = false
assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(
- Enabled(
- isRealSnapshot = false,
- thumbnail = null,
- )
- )
+ .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = null))
}
@Test
@@ -103,17 +98,12 @@
recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
tasksRepository.seedTasks(listOf(task))
tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(listOf(TASK_ID))
+ tasksRepository.setVisibleTasks(setOf(TASK_ID))
thumbnailData.isRealSnapshot = true
task.isLocked = false
assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(
- Enabled(
- isRealSnapshot = true,
- thumbnail = thumbnailData.thumbnail,
- )
- )
+ .isEqualTo(Enabled(isRealSnapshot = true, thumbnail = thumbnailData.thumbnail))
}
@Test
@@ -122,17 +112,12 @@
recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
tasksRepository.seedTasks(listOf(task))
tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(listOf(TASK_ID))
+ tasksRepository.setVisibleTasks(setOf(TASK_ID))
thumbnailData.isRealSnapshot = true
task.isLocked = true
assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(
- Enabled(
- isRealSnapshot = false,
- thumbnail = thumbnailData.thumbnail,
- )
- )
+ .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
}
@Test
@@ -141,17 +126,12 @@
recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
tasksRepository.seedTasks(listOf(task))
tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(listOf(TASK_ID))
+ tasksRepository.setVisibleTasks(setOf(TASK_ID))
thumbnailData.isRealSnapshot = false
task.isLocked = false
assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(
- Enabled(
- isRealSnapshot = false,
- thumbnail = thumbnailData.thumbnail,
- )
- )
+ .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
}
@Test
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 5acff06..8dce723 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -187,7 +187,7 @@
@ViewDebug.ExportedProperty(category = "launcher")
private DotInfo mDotInfo;
private DotRenderer mDotRenderer;
- private Locale mCurrentLocale;
+ private String mCurrentLanguage;
@ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
protected DotRenderer.DrawParams mDotParams;
private Animator mDotScaleAnim;
@@ -298,7 +298,7 @@
mDotParams = new DotRenderer.DrawParams();
- mCurrentLocale = context.getResources().getConfiguration().locale;
+ mCurrentLanguage = context.getResources().getConfiguration().locale.getLanguage();
setEllipsize(TruncateAt.END);
setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
setTextAlpha(1f);
@@ -494,7 +494,7 @@
}
protected boolean isCurrentLanguageEnglish() {
- return mCurrentLocale.equals(Locale.US);
+ return mCurrentLanguage.equals(Locale.ENGLISH.getLanguage());
}
@UiThread
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 4da7c27..4537785 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -23,6 +23,7 @@
import com.android.launcher3.util.PluginManagerWrapper;
import com.android.launcher3.util.ScreenOnTracker;
import com.android.launcher3.util.SettingsCache;
+import com.android.launcher3.util.window.RefreshRateTracker;
import com.android.launcher3.widget.custom.CustomWidgetManager;
import dagger.BindsInstance;
@@ -37,6 +38,7 @@
*/
public interface LauncherBaseAppComponent {
DaggerSingletonTracker getDaggerSingletonTracker();
+ RefreshRateTracker getRefreshRateTracker();
InstallSessionHelper getInstallSessionHelper();
ScreenOnTracker getScreenOnTracker();
SettingsCache getSettingsCache();
diff --git a/src/com/android/launcher3/util/DaggerSingletonTracker.java b/src/com/android/launcher3/util/DaggerSingletonTracker.java
index 2946da1..b7a88db 100644
--- a/src/com/android/launcher3/util/DaggerSingletonTracker.java
+++ b/src/com/android/launcher3/util/DaggerSingletonTracker.java
@@ -16,6 +16,8 @@
package com.android.launcher3.util;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
import com.android.launcher3.dagger.LauncherAppSingleton;
import java.util.ArrayList;
@@ -31,7 +33,9 @@
@LauncherAppSingleton
public class DaggerSingletonTracker implements SafeCloseable {
- private final ArrayList<SafeCloseable> mLauncherAppSingletons = new ArrayList<>();
+ private final ArrayList<SafeCloseable> mCloseables = new ArrayList<>();
+
+ private boolean mClosed = false;
@Inject
DaggerSingletonTracker() {
@@ -44,14 +48,21 @@
* {@link MainThreadInitializedObject.SandboxContext#onDestroy()}
*/
public void addCloseable(SafeCloseable closeable) {
- mLauncherAppSingletons.add(closeable);
+ MAIN_EXECUTOR.execute(() -> {
+ if (mClosed) {
+ closeable.close();
+ } else {
+ mCloseables.add(closeable);
+ }
+ });
}
@Override
public void close() {
+ mClosed = true;
// Destroy in reverse order
- for (int i = mLauncherAppSingletons.size() - 1; i >= 0; i--) {
- mLauncherAppSingletons.get(i).close();
+ for (int i = mCloseables.size() - 1; i >= 0; i--) {
+ mCloseables.get(i).close();
}
}
}
diff --git a/src/com/android/launcher3/util/ExecutorUtil.java b/src/com/android/launcher3/util/ExecutorUtil.java
deleted file mode 100644
index efc0eec..0000000
--- a/src/com/android/launcher3/util/ExecutorUtil.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.os.Looper;
-
-import java.util.concurrent.ExecutionException;
-
-public final class ExecutorUtil {
-
- /**
- * Executes runnable on {@link Looper#getMainLooper()}, otherwise fails with an exception.
- */
- public static void executeSyncOnMainOrFail(Runnable runnable) {
- try {
- MAIN_EXECUTOR.submit(runnable).get();
- } catch (InterruptedException | ExecutionException e) {
- throw new RuntimeException(e);
- }
- }
-}
diff --git a/src/com/android/launcher3/util/ScreenOnTracker.java b/src/com/android/launcher3/util/ScreenOnTracker.java
index 3582ad8..50be98b 100644
--- a/src/com/android/launcher3/util/ScreenOnTracker.java
+++ b/src/com/android/launcher3/util/ScreenOnTracker.java
@@ -68,7 +68,7 @@
private void init(DaggerSingletonTracker tracker) {
mIsScreenOn = true;
mReceiver.register(mContext, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
- ExecutorUtil.executeSyncOnMainOrFail(() -> tracker.addCloseable(this));
+ tracker.addCloseable(this);
}
@Override
diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java
index a1ed499..29d5032 100644
--- a/src/com/android/launcher3/util/SettingsCache.java
+++ b/src/com/android/launcher3/util/SettingsCache.java
@@ -94,7 +94,7 @@
SettingsCache(@ApplicationContext Context context, DaggerSingletonTracker tracker) {
super(new Handler(Looper.getMainLooper()));
mResolver = context.getContentResolver();
- ExecutorUtil.executeSyncOnMainOrFail(() -> tracker.addCloseable(this));
+ tracker.addCloseable(this);
}
@Override
diff --git a/src/com/android/launcher3/util/window/RefreshRateTracker.java b/src/com/android/launcher3/util/window/RefreshRateTracker.java
index 7814617..e3397d4 100644
--- a/src/com/android/launcher3/util/window/RefreshRateTracker.java
+++ b/src/com/android/launcher3/util/window/RefreshRateTracker.java
@@ -26,25 +26,34 @@
import androidx.annotation.WorkerThread;
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.SafeCloseable;
+import javax.inject.Inject;
+
/**
* Utility class to track refresh rate of the current device
*/
+@LauncherAppSingleton
public class RefreshRateTracker implements DisplayListener, SafeCloseable {
- private static final MainThreadInitializedObject<RefreshRateTracker> INSTANCE =
- new MainThreadInitializedObject<>(RefreshRateTracker::new);
+ private static final DaggerSingletonObject<RefreshRateTracker> INSTANCE =
+ new DaggerSingletonObject<>(LauncherAppComponent::getRefreshRateTracker);
private int mSingleFrameMs = 1;
private final DisplayManager mDM;
- private RefreshRateTracker(Context context) {
+ @Inject
+ RefreshRateTracker(@ApplicationContext Context context, DaggerSingletonTracker tracker) {
mDM = context.getSystemService(DisplayManager.class);
updateSingleFrameMs();
mDM.registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler());
+ tracker.addCloseable(this);
}
/**
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
index 4aeac76..9dddc18 100644
--- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
+++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.Flags.enableSmartspaceAsAWidget;
import static com.android.launcher3.model.data.LauncherAppWidgetInfo.CUSTOM_WIDGET_ID;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX;
import android.appwidget.AppWidgetManager;
@@ -38,7 +39,6 @@
import com.android.launcher3.dagger.LauncherBaseAppComponent;
import com.android.launcher3.util.DaggerSingletonObject;
import com.android.launcher3.util.DaggerSingletonTracker;
-import com.android.launcher3.util.ExecutorUtil;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.PluginManagerWrapper;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
@@ -90,27 +90,23 @@
mCustomWidgets = new ArrayList<>();
pluginManager.addPluginListener(this, CustomWidgetPlugin.class, true);
-
- ExecutorUtil.executeSyncOnMainOrFail(() -> {
- if (enableSmartspaceAsAWidget()) {
- for (String s: context.getResources()
- .getStringArray(R.array.custom_widget_providers)) {
- try {
- Class<?> cls = Class.forName(s);
- CustomWidgetPlugin plugin = (CustomWidgetPlugin)
- cls.getDeclaredConstructor(Context.class).newInstance(context);
- onPluginConnected(plugin, context);
- } catch (ClassNotFoundException | InstantiationException
- | IllegalAccessException
- | ClassCastException | NoSuchMethodException
- | InvocationTargetException e) {
- Log.e(TAG, "Exception found when trying to add custom widgets: " + e);
- }
+ if (enableSmartspaceAsAWidget()) {
+ for (String s: context.getResources()
+ .getStringArray(R.array.custom_widget_providers)) {
+ try {
+ Class<?> cls = Class.forName(s);
+ CustomWidgetPlugin plugin = (CustomWidgetPlugin)
+ cls.getDeclaredConstructor(Context.class).newInstance(context);
+ MAIN_EXECUTOR.execute(() -> onPluginConnected(plugin, context));
+ } catch (ClassNotFoundException | InstantiationException
+ | IllegalAccessException
+ | ClassCastException | NoSuchMethodException
+ | InvocationTargetException e) {
+ Log.e(TAG, "Exception found when trying to add custom widgets: " + e);
}
}
-
- tracker.addCloseable(() -> pluginManager.removePluginListener(this));
- });
+ }
+ tracker.addCloseable(() -> pluginManager.removePluginListener(this));
}
@Override
diff --git a/tests/Android.bp b/tests/Android.bp
index 9f62d02..9667277 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -98,6 +98,8 @@
"com_android_launcher3_flags_lib",
"com_android_wm_shell_flags_lib",
"android.appwidget.flags-aconfig-java",
+ "platform-parametric-runner-lib",
+ "kotlin-reflect",
],
manifest: "AndroidManifest-common.xml",
platform_apis: true,
@@ -111,6 +113,9 @@
asset_dirs: ["assets"],
// TODO(b/319712088): re-enable use_resource_processor
use_resource_processor: false,
+ static_libs: [
+ "kotlin-reflect",
+ ],
}
android_test {
@@ -193,10 +198,7 @@
name: "Launcher3RoboTests",
srcs: [
":launcher3-robo-src",
-
- // Test util classes
":launcher-testing-helpers-robo",
- ":launcher-testing-shared",
],
exclude_srcs: [
//"src/com/android/launcher3/util/CellContentDimensionsTest.kt", // Failing - b/316553889
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/DaggerSingletonDeadlockTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/DaggerSingletonDeadlockTest.kt
new file mode 100644
index 0000000..642c628
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DaggerSingletonDeadlockTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import androidx.test.filters.SmallTest
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import java.util.concurrent.TimeUnit.SECONDS
+import kotlin.reflect.KFunction
+import kotlin.reflect.full.memberFunctions
+import org.junit.After
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class DaggerSingletonDeadlockTest(val method: KFunction<*>, val methodName: String) {
+
+ private val context = SandboxModelContext()
+
+ @After
+ fun tearDown() {
+ context.onDestroy()
+ }
+
+ /** Test to verify that the object can be created successfully on the main thread. */
+ @Test
+ fun objectCreationOnMainThread() {
+ Executors.MAIN_EXECUTOR.submit {
+ method.call(context.appComponent).also(Assert::assertNotNull)
+ }
+ .get(10, SECONDS)
+ }
+
+ /**
+ * Test to verify that the object can be created successfully on the background thread, when the
+ * main thread is blocked.
+ */
+ @Test
+ fun objectCreationOnBackgroundThread() {
+ TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {
+ Executors.THREAD_POOL_EXECUTOR.submit {
+ method.call(context.appComponent).also(Assert::assertNotNull)
+ }
+ .get(10, SECONDS)
+ }
+ }
+
+ companion object {
+ @Parameters(name = "{1}")
+ @JvmStatic
+ fun getTestMethods() =
+ LauncherAppComponent::class
+ .memberFunctions
+ .filter { it.parameters.size == 1 }
+ .map {
+ arrayOf(it, if (it.name.startsWith("get")) it.name.substring(3) else it.name)
+ }
+ }
+}