Merge "Fix test failures in AbsSwipeUpHandlerTestCase" into main
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 782a705..6367a01 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -361,7 +361,11 @@
<dimen name="taskbar_running_app_indicator_width">12dp</dimen>
<dimen name="taskbar_running_app_indicator_top_margin">4dp</dimen>
<dimen name="taskbar_minimized_app_indicator_width">6dp</dimen>
- <dimen name="taskbar_overflow_button_preview_stroke">2dp</dimen>
+ <dimen name="taskbar_overflow_item_icon_size_default">22dp</dimen>
+ <dimen name="taskbar_overflow_item_icon_size_scaled_down">15dp</dimen>
+ <dimen name="taskbar_overflow_item_icon_stroke_width_default">2dp</dimen>
+ <dimen name="taskbar_overflow_leave_behind_size_default">18dp</dimen>
+ <dimen name="taskbar_overflow_leave_behind_size_scaled_down">15dp</dimen>
<!-- Transient taskbar -->
<dimen name="transient_taskbar_padding">12dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index e160f82..87a82f0 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -143,6 +143,7 @@
}
companion object {
+ /** Change modes that represent a task becoming visible / launching in Desktop mode. */
val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
private val launchBoundsAnimationDef =
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
new file mode 100644
index 0000000..e32bcd1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.desktop
+
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
+import android.window.DesktopModeFlags
+import android.window.RemoteTransition
+import android.window.TransitionFilter
+import android.window.TransitionFilter.CONTAINER_ORDER_TOP
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.quickstep.SystemUiProxy
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+
+/** Manages transitions related to app launches in Desktop Mode. */
+class DesktopAppLaunchTransitionManager(
+ private val context: Context,
+ private val systemUiProxy: SystemUiProxy,
+) {
+ private var remoteWindowLimitUnminimizeTransition: RemoteTransition? = null
+
+ /**
+ * Register a [RemoteTransition] supporting Desktop app launches, and window limit
+ * minimizations.
+ */
+ fun registerTransitions() {
+ if (!shouldRegisterTransitions()) {
+ return
+ }
+ remoteWindowLimitUnminimizeTransition =
+ RemoteTransition(
+ DesktopAppLaunchTransition(context, MAIN_EXECUTOR, AppLaunchType.UNMINIMIZE)
+ )
+ systemUiProxy.registerRemoteTransition(
+ remoteWindowLimitUnminimizeTransition,
+ buildAppLaunchFilter(),
+ )
+ }
+
+ /**
+ * Unregister the [RemoteTransition] supporting Desktop app launches and window limit
+ * minimizations.
+ */
+ fun unregisterTransitions() {
+ if (!shouldRegisterTransitions()) {
+ return
+ }
+ systemUiProxy.unregisterRemoteTransition(remoteWindowLimitUnminimizeTransition)
+ remoteWindowLimitUnminimizeTransition = null
+ }
+
+ private fun shouldRegisterTransitions(): Boolean =
+ DesktopModeStatus.canEnterDesktopMode(context) &&
+ DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue
+
+ companion object {
+ private fun buildAppLaunchFilter(): TransitionFilter {
+ val openRequirement =
+ TransitionFilter.Requirement().apply {
+ mActivityType = ACTIVITY_TYPE_STANDARD
+ mWindowingMode = WINDOWING_MODE_FREEFORM
+ mModes = DesktopAppLaunchTransition.LAUNCH_CHANGE_MODES
+ mMustBeTask = true
+ mOrder = CONTAINER_ORDER_TOP
+ }
+ return TransitionFilter().apply {
+ mTypeSet = DesktopAppLaunchTransition.LAUNCH_CHANGE_MODES
+ mRequirements = arrayOf(openRequirement)
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index b1cb2c6..4a94be7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -362,20 +362,21 @@
// This method can be called before init() is called.
return;
}
- if (mControllers.uiController.isIconAlignedWithHotseat()
- && !mTaskbarLauncherStateController.isAnimatingToLauncher()) {
- // Only animate the nav buttons while home and not animating home, otherwise let
- // the TaskbarViewController handle it.
- mControllers.navbarButtonsViewController
- .getTaskbarNavButtonTranslationYForInAppDisplay()
- .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY()
- * mTaskbarInAppDisplayProgress.value);
- mControllers.navbarButtonsViewController
- .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
-
+ if (mControllers.uiController.isIconAlignedWithHotseat()) {
+ if (!mTaskbarLauncherStateController.isAnimatingToLauncher()) {
+ // Only animate the nav buttons while home and not animating home, otherwise let
+ // the TaskbarViewController handle it.
+ mControllers.navbarButtonsViewController
+ .getTaskbarNavButtonTranslationYForInAppDisplay()
+ .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY()
+ * mTaskbarInAppDisplayProgress.value);
+ mControllers.navbarButtonsViewController
+ .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
+ }
if (isBubbleBarEnabled()) {
mControllers.bubbleControllers.ifPresent(
- c -> c.bubbleStashController.setInAppDisplayOverrideProgress(progress));
+ c -> c.bubbleStashController.setInAppDisplayOverrideProgress(
+ mTaskbarInAppDisplayProgress.value));
}
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 82acc0c..d7e5c61 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -418,7 +418,7 @@
/** Called when the visibility of the bubble bar changed. */
public void bubbleBarVisibilityChanged(boolean isVisible) {
mControllers.uiController.adjustHotseatForBubbleBar(isVisible);
- mControllers.taskbarViewController.resetIconAlignmentController();
+ mControllers.taskbarViewController.adjustTaskbarForBubbleBar();
}
public void init(@NonNull TaskbarSharedState sharedState) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
index 126e9bb..712478e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
@@ -16,17 +16,28 @@
package com.android.launcher3.taskbar;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
import android.content.Context;
+import android.graphics.BlendMode;
+import android.graphics.BlendModeColorFilter;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.IntProperty;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
+import androidx.core.graphics.ColorUtils;
+import com.android.app.animation.Interpolators;
import com.android.launcher3.R;
import com.android.launcher3.Reorderable;
import com.android.launcher3.Utilities;
@@ -45,8 +56,104 @@
* each other in counter clockwise manner (icons of tasks partially overlapping with each other).
*/
public class TaskbarOverflowView extends FrameLayout implements Reorderable {
+ private static final int ALPHA_TRANSPARENT = 0;
+ private static final int ALPHA_OPAQUE = 255;
+ private static final long ANIMATION_DURATION_APPS_TO_LEAVE_BEHIND = 300L;
+ private static final long ANIMATION_DURATION_LEAVE_BEHIND_TO_APPS = 500L;
+ private static final long ANIMATION_SET_DURATION = 1000L;
+ private static final long ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION = 500L;
+ private static final long ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION = 600L;
+ private static final long ITEM_ICON_SIZE_ANIMATION_DURATION = 500L;
+ private static final long ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION = 500L;
+ private static final long LEAVE_BEHIND_ANIMATIONS_DELAY = 500L;
+ private static final long LEAVE_BEHIND_OPACITY_ANIMATION_DURATION = 100L;
+ private static final long LEAVE_BEHIND_SIZE_ANIMATION_DURATION = 500L;
private static final int MAX_ITEMS_IN_PREVIEW = 4;
+ private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_CENTER_OFFSET =
+ new FloatProperty<>("itemIconCenterOffset") {
+ @Override
+ public Float get(TaskbarOverflowView view) {
+ return view.mItemIconCenterOffset;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, float value) {
+ view.mItemIconCenterOffset = value;
+ view.invalidate();
+ }
+ };
+
+ private static final IntProperty<TaskbarOverflowView> ITEM_ICON_COLOR_FILTER_OPACITY =
+ new IntProperty<>("itemIconColorFilterOpacity") {
+ @Override
+ public Integer get(TaskbarOverflowView view) {
+ return view.mItemIconColorFilterOpacity;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, int value) {
+ view.mItemIconColorFilterOpacity = value;
+ view.invalidate();
+ }
+ };
+
+ private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_SIZE =
+ new FloatProperty<>("itemIconSize") {
+ @Override
+ public Float get(TaskbarOverflowView view) {
+ return view.mItemIconSize;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, float value) {
+ view.mItemIconSize = value;
+ view.invalidate();
+ }
+ };
+
+ private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_STROKE_WIDTH =
+ new FloatProperty<>("itemIconStrokeWidth") {
+ @Override
+ public Float get(TaskbarOverflowView view) {
+ return view.mItemIconStrokeWidth;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, float value) {
+ view.mItemIconStrokeWidth = value;
+ view.invalidate();
+ }
+ };
+
+ private static final IntProperty<TaskbarOverflowView> LEAVE_BEHIND_OPACITY =
+ new IntProperty<>("leaveBehindOpacity") {
+ @Override
+ public Integer get(TaskbarOverflowView view) {
+ return view.mLeaveBehindOpacity;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, int value) {
+ view.mLeaveBehindOpacity = value;
+ view.invalidate();
+ }
+ };
+
+ private static final FloatProperty<TaskbarOverflowView> LEAVE_BEHIND_SIZE =
+ new FloatProperty<>("leaveBehindSize") {
+ @Override
+ public Float get(TaskbarOverflowView view) {
+ return view.mLeaveBehindSize;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, float value) {
+ view.mLeaveBehindSize = value;
+ view.invalidate();
+ }
+ };
+
private boolean mIsRtlLayout;
private final List<Task> mItems = new ArrayList<Task>();
private int mIconSize;
@@ -56,11 +163,24 @@
private float mScaleForReorderBounce = 1f;
private int mItemBackgroundColor;
private int mLeaveBehindColor;
- private float mItemPreviewStrokeWidth;
// Active means the overflow icon has been pressed, which replaces the app icons with the
// leave-behind circle and shows the KQS UI.
private boolean mIsActive = false;
+ private ValueAnimator mStateTransitionAnimationWrapper;
+
+ private float mItemIconCenterOffsetDefault;
+ private float mItemIconCenterOffset; // [0..mItemIconCenterOffsetDefault]
+ private int mItemIconColorFilterOpacity; // [ALPHA_TRANSPARENT..ALPHA_OPAQUE]
+ private float mItemIconSizeDefault;
+ private float mItemIconSizeScaledDown;
+ private float mItemIconSize; // [mItemIconSizeScaledDown..mItemIconSizeDefault]
+ private float mItemIconStrokeWidthDefault;
+ private float mItemIconStrokeWidth; // [0..mItemIconStrokeWidthDefault]
+ private int mLeaveBehindOpacity; // [ALPHA_TRANSPARENT..ALPHA_OPAQUE]
+ private float mLeaveBehindSizeScaledDown;
+ private float mLeaveBehindSizeDefault;
+ private float mLeaveBehindSize; // [mLeaveBehindSizeScaledDown..mLeaveBehindSizeDefault]
public TaskbarOverflowView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -87,6 +207,12 @@
icon.mIconSize = iconSize;
icon.mPadding = padding;
+
+ final float radius = iconSize / 2f - padding;
+ final float size = radius + icon.mItemIconStrokeWidth;
+ icon.mItemIconCenterOffsetDefault = radius - size / 2 - icon.mItemIconStrokeWidth;
+ icon.mItemIconCenterOffset = icon.mItemIconCenterOffsetDefault;
+
return icon;
}
@@ -95,8 +221,22 @@
mItemBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mItemBackgroundColor = getContext().getColor(R.color.taskbar_background);
mLeaveBehindColor = Themes.getAttrColor(getContext(), android.R.attr.textColorTertiary);
- mItemPreviewStrokeWidth = getResources().getDimension(
- R.dimen.taskbar_overflow_button_preview_stroke);
+
+ mItemIconSizeDefault = getResources().getDimension(
+ R.dimen.taskbar_overflow_item_icon_size_default);
+ mItemIconSizeScaledDown = getResources().getDimension(
+ R.dimen.taskbar_overflow_item_icon_size_scaled_down);
+ mItemIconSize = mItemIconSizeDefault;
+
+ mItemIconStrokeWidthDefault = getResources().getDimension(
+ R.dimen.taskbar_overflow_item_icon_stroke_width_default);
+ mItemIconStrokeWidth = mItemIconStrokeWidthDefault;
+
+ mLeaveBehindSizeDefault = getResources().getDimension(
+ R.dimen.taskbar_overflow_leave_behind_size_default);
+ mLeaveBehindSizeScaledDown = getResources().getDimension(
+ R.dimen.taskbar_overflow_leave_behind_size_scaled_down);
+ mLeaveBehindSize = mLeaveBehindSizeScaledDown;
setWillNotDraw(false);
}
@@ -105,16 +245,14 @@
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
- if (mIsActive) {
- drawLeaveBehindCircle(canvas);
- } else {
- drawAppIcons(canvas);
- }
+ drawAppIcons(canvas);
+ drawLeaveBehindCircle(canvas);
}
private void drawAppIcons(@NonNull Canvas canvas) {
mItemBackgroundPaint.setColor(mItemBackgroundColor);
float radius = mIconSize / 2f - mPadding;
+ int adjustedItemIconSize = Math.round(mItemIconSize);
int itemsToShow = Math.min(mItems.size(), MAX_ITEMS_IN_PREVIEW);
for (int i = itemsToShow - 1; i >= 0; --i) {
@@ -123,36 +261,33 @@
continue;
}
- // Set the item icon size so two items fit within the overflow icon with stroke width
- // included, and overlap of 4 stroke width sizes between base item preview items.
- // 2 * strokeWidth + 2 * itemIconSize - 4 * strokeWidth = iconSize = 2 * radius.
- float itemIconSize = radius + mItemPreviewStrokeWidth;
- // Offset item icon from center so item icon stroke edge matches the parent icon edge.
- float itemCenterOffset = radius - itemIconSize / 2 - mItemPreviewStrokeWidth;
-
- float itemCenterX = getItemXOffset(itemCenterOffset, mIsRtlLayout, i, itemsToShow);
- float itemCenterY = getItemYOffset(itemCenterOffset, i, itemsToShow);
+ float itemCenterX = getItemXOffset(mItemIconCenterOffset, mIsRtlLayout, i, itemsToShow);
+ float itemCenterY = getItemYOffset(mItemIconCenterOffset, i, itemsToShow);
Drawable iconCopy = icon.getConstantState().newDrawable().mutate();
- iconCopy.setBounds(0, 0, (int) itemIconSize, (int) itemIconSize);
+ iconCopy.setBounds(0, 0, adjustedItemIconSize, adjustedItemIconSize);
+ iconCopy.setColorFilter(new BlendModeColorFilter(
+ ColorUtils.setAlphaComponent(mLeaveBehindColor, mItemIconColorFilterOpacity),
+ BlendMode.SRC_ATOP));
canvas.save();
- float itemIconRadius = itemIconSize / 2;
+ float itemIconRadius = adjustedItemIconSize / 2f;
canvas.translate(
mPadding + itemCenterX + radius - itemIconRadius,
mPadding + itemCenterY + radius - itemIconRadius);
canvas.drawCircle(itemIconRadius, itemIconRadius,
- itemIconRadius + mItemPreviewStrokeWidth, mItemBackgroundPaint);
+ itemIconRadius + mItemIconStrokeWidth, mItemBackgroundPaint);
iconCopy.draw(canvas);
canvas.restore();
}
}
private void drawLeaveBehindCircle(@NonNull Canvas canvas) {
- mItemBackgroundPaint.setColor(mLeaveBehindColor);
+ mItemBackgroundPaint.setColor(
+ ColorUtils.setAlphaComponent(mLeaveBehindColor, mLeaveBehindOpacity));
- final var xyCenter = mIconSize / 2f;
- canvas.drawCircle(xyCenter, xyCenter, mIconSize / 4f, mItemBackgroundPaint);
+ final float xyCenter = mIconSize / 2f;
+ canvas.drawCircle(xyCenter, xyCenter, mLeaveBehindSize / 2f, mItemBackgroundPaint);
}
/**
@@ -203,10 +338,98 @@
* @param isActive The next state of the view.
*/
public void setIsActive(boolean isActive) {
- if (mIsActive != isActive) {
- mIsActive = isActive;
- invalidate();
+ if (mIsActive == isActive) {
+ return;
}
+ mIsActive = isActive;
+
+ if (mStateTransitionAnimationWrapper != null
+ && mStateTransitionAnimationWrapper.isRunning()) {
+ mStateTransitionAnimationWrapper.reverse();
+ return;
+ }
+
+ final AnimatorSet stateTransitionAnimation = getStateTransitionAnimation();
+ mStateTransitionAnimationWrapper = ValueAnimator.ofFloat(0, 1f);
+ mStateTransitionAnimationWrapper.setDuration(mIsActive
+ ? ANIMATION_DURATION_APPS_TO_LEAVE_BEHIND
+ : ANIMATION_DURATION_LEAVE_BEHIND_TO_APPS);
+ mStateTransitionAnimationWrapper.setInterpolator(
+ mIsActive ? Interpolators.STANDARD : Interpolators.EMPHASIZED);
+ mStateTransitionAnimationWrapper.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mStateTransitionAnimationWrapper = null;
+ }
+ });
+ mStateTransitionAnimationWrapper.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ stateTransitionAnimation.setCurrentPlayTime(
+ (long) (ANIMATION_SET_DURATION * animator.getAnimatedFraction()));
+ }
+ });
+ mStateTransitionAnimationWrapper.start();
+ }
+
+ private AnimatorSet getStateTransitionAnimation() {
+ final AnimatorSet animation = new AnimatorSet();
+ animation.setInterpolator(Interpolators.LINEAR);
+ animation.playTogether(
+ buildAnimator(ITEM_ICON_CENTER_OFFSET, 0f, mItemIconCenterOffsetDefault,
+ ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION, 0L,
+ ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION),
+ buildAnimator(ITEM_ICON_COLOR_FILTER_OPACITY, ALPHA_OPAQUE, ALPHA_TRANSPARENT,
+ ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION, 0L,
+ ANIMATION_SET_DURATION - ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION),
+ buildAnimator(ITEM_ICON_SIZE, mItemIconSizeScaledDown, mItemIconSizeDefault,
+ ITEM_ICON_SIZE_ANIMATION_DURATION, 0L,
+ ITEM_ICON_SIZE_ANIMATION_DURATION),
+ buildAnimator(ITEM_ICON_STROKE_WIDTH, 0f, mItemIconStrokeWidthDefault,
+ ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION, 0L,
+ ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION),
+ buildAnimator(LEAVE_BEHIND_OPACITY, ALPHA_OPAQUE, ALPHA_TRANSPARENT,
+ LEAVE_BEHIND_OPACITY_ANIMATION_DURATION, LEAVE_BEHIND_ANIMATIONS_DELAY,
+ ANIMATION_SET_DURATION - LEAVE_BEHIND_ANIMATIONS_DELAY
+ - LEAVE_BEHIND_OPACITY_ANIMATION_DURATION),
+ buildAnimator(LEAVE_BEHIND_SIZE, mLeaveBehindSizeDefault,
+ mLeaveBehindSizeScaledDown, LEAVE_BEHIND_SIZE_ANIMATION_DURATION,
+ LEAVE_BEHIND_ANIMATIONS_DELAY, 0L)
+ );
+ return animation;
+ }
+
+ private ObjectAnimator buildAnimator(IntProperty<TaskbarOverflowView> property,
+ int finalValueWhenAnimatingToLeaveBehind, int finalValueWhenAnimatingToAppIcons,
+ long duration, long delayWhenAnimatingToLeaveBehind,
+ long delayWhenAnimatingToAppIcons) {
+ final ObjectAnimator animator = ObjectAnimator.ofInt(this, property,
+ mIsActive ? finalValueWhenAnimatingToLeaveBehind
+ : finalValueWhenAnimatingToAppIcons);
+ applyTiming(animator, duration, delayWhenAnimatingToLeaveBehind,
+ delayWhenAnimatingToAppIcons);
+ return animator;
+ }
+
+ private ObjectAnimator buildAnimator(FloatProperty<TaskbarOverflowView> property,
+ float finalValueWhenAnimatingToLeaveBehind, float finalValueWhenAnimatingToAppIcons,
+ long duration, long delayWhenAnimatingToLeaveBehind,
+ long delayWhenAnimatingToAppIcons) {
+ final ObjectAnimator animator = ObjectAnimator.ofFloat(this, property,
+ mIsActive ? finalValueWhenAnimatingToLeaveBehind
+ : finalValueWhenAnimatingToAppIcons);
+ applyTiming(animator, duration, delayWhenAnimatingToLeaveBehind,
+ delayWhenAnimatingToAppIcons);
+ return animator;
+ }
+
+ private void applyTiming(ObjectAnimator animator, long duration,
+ long delayWhenAnimatingToLeaveBehind,
+ long delayWhenAnimatingToAppIcons) {
+ animator.setDuration(duration);
+ animator.setStartDelay(
+ mIsActive ? delayWhenAnimatingToLeaveBehind : delayWhenAnimatingToAppIcons);
}
@Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 55bcb23..8816a6d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -120,7 +120,8 @@
private boolean mShouldTryStartAlign;
- private final int mMaxNumIcons;
+ private int mMaxNumIcons = 0;
+ private int mIdealNumIcons = 0;
private final int mAllAppsButtonTranslationOffset;
@@ -188,8 +189,6 @@
// TODO: Disable touch events on QSB otherwise it can crash.
mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
-
- mMaxNumIcons = calculateMaxNumIcons();
}
/**
@@ -200,11 +199,15 @@
int availableWidth = deviceProfile.widthPx;
int defaultEdgeMargin =
(int) getResources().getDimension(deviceProfile.inv.inlineNavButtonsEndSpacing);
+ int spaceForBubbleBar =
+ Math.round(mControllerCallbacks.getBubbleBarMaxCollapsedWidthIfVisible());
// Reserve space required for edge margins, or for navbar if shown. If task bar needs to be
// center aligned with nav bar shown, reserve space on both sides.
- availableWidth -= Math.max(defaultEdgeMargin, deviceProfile.hotseatBarEndOffset);
- availableWidth -= Math.max(defaultEdgeMargin,
+ availableWidth -=
+ Math.max(defaultEdgeMargin + spaceForBubbleBar, deviceProfile.hotseatBarEndOffset);
+ availableWidth -= Math.max(
+ defaultEdgeMargin + (mShouldTryStartAlign ? 0 : spaceForBubbleBar),
mShouldTryStartAlign ? 0 : deviceProfile.hotseatBarEndOffset);
// The space taken by an item icon used during layout.
@@ -231,6 +234,21 @@
return Math.floorDiv(availableWidth, iconSize) + additionalIcons;
}
+ /**
+ * Recalculates the max number of icons the taskbar view can show without entering overflow.
+ * Returns whether the max number of icons changed and the change affects the number of icons
+ * that should be shown in the taskbar.
+ */
+ boolean updateMaxNumIcons() {
+ if (!Flags.taskbarOverflow()) {
+ return false;
+ }
+ int oldMaxNumIcons = mMaxNumIcons;
+ mMaxNumIcons = calculateMaxNumIcons();
+ return oldMaxNumIcons != mMaxNumIcons
+ && (mIdealNumIcons > oldMaxNumIcons || mIdealNumIcons > mMaxNumIcons);
+ }
+
@Override
public void setVisibility(int visibility) {
boolean changed = getVisibility() != visibility;
@@ -328,6 +346,10 @@
&& mActivityContext.getTaskbarFeatureEvaluator().getSupportsPinningPopup()) {
setOnTouchListener(mControllerCallbacks.getTaskbarTouchListener());
}
+
+ if (Flags.taskbarOverflow()) {
+ mMaxNumIcons = calculateMaxNumIcons();
+ }
}
private void removeAndRecycle(View view) {
@@ -460,8 +482,9 @@
}
}
- overflowSize =
- nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded - mMaxNumIcons;
+ mIdealNumIcons = nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded;
+ overflowSize = mIdealNumIcons - mMaxNumIcons;
+
if (overflowSize > 0 && mTaskbarOverflowView != null) {
addView(mTaskbarOverflowView, nextViewIndex++);
} else if (mTaskbarOverflowView != null) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 834f92e..f65f307 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -137,6 +137,17 @@
return null;
}
+ /**
+ * Get the max bubble bar collapsed width for the current bubble bar visibility state. Used to
+ * reserve space for the bubble bar when transitioning taskbar view into overflow.
+ */
+ public float getBubbleBarMaxCollapsedWidthIfVisible() {
+ return mControllers.bubbleControllers
+ .filter(c -> !c.bubbleBarViewController.isHiddenForNoBubbles())
+ .map(c -> c.bubbleBarViewController.getCollapsedWidthWithMaxVisibleBubbles())
+ .orElse(0f);
+ }
+
/** Returns true if bubble bar controllers present and enabled in persistent taskbar. */
public boolean isBubbleBarEnabledInPersistentTaskbar() {
return Flags.enableBubbleBarInPersistentTaskBar()
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 494c472..cebabff 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -779,9 +779,16 @@
}
}
- /** Resets the icon alignment controller so that it can be recreated again later. */
- void resetIconAlignmentController() {
+ /**
+ * Resets the icon alignment controller so that it can be recreated again later, and updates
+ * the list of icons shown in the taskbar if the bubble bar visibility changes the taskbar
+ * overflow state.
+ */
+ void adjustTaskbarForBubbleBar() {
mIconAlignControllerLazy = null;
+ if (mTaskbarView.updateMaxNumIcons()) {
+ commitRunningAppsToUI();
+ }
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 350f56f..c5c2d69 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -1290,10 +1290,14 @@
// If there are more than 2 bubbles, the first 2 should be visible when collapsed,
// excluding the overflow.
return bubbleChildCount >= MAX_VISIBLE_BUBBLES_COLLAPSED
- ? getScaledIconSize() + mIconOverlapAmount + horizontalPadding
+ ? getCollapsedWidthWithMaxVisibleBubbles()
: getScaledIconSize() + horizontalPadding;
}
+ float getCollapsedWidthWithMaxVisibleBubbles() {
+ return getScaledIconSize() + mIconOverlapAmount + 2 * mBubbleBarPadding;
+ }
+
/** Returns the child count excluding the overflow if it's present. */
int getBubbleChildCount() {
return hasOverflow() ? getChildCount() - 1 : getChildCount();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 2dddb16..d842138 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -473,6 +473,13 @@
}
/**
+ * @return the max collapsed width for the bubble bar.
+ */
+ public float getCollapsedWidthWithMaxVisibleBubbles() {
+ return mBarView.getCollapsedWidthWithMaxVisibleBubbles();
+ }
+
+ /**
* @return {@code true} if bubble bar is on the left edge of the screen, {@code false} if on
* the right
*/
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 7b20eea..908e97c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -21,6 +21,7 @@
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.animation.ValueAnimator
+import com.android.app.animation.InterpolatorsAndroidX
import com.android.launcher3.R
import com.android.systemui.util.addListener
@@ -35,7 +36,8 @@
) {
private companion object {
- const val ANIMATION_DURATION_MS = 250L
+ const val EXPAND_ANIMATION_DURATION_MS = 400L
+ const val COLLAPSE_ANIMATION_DURATION_MS = 350L
}
private var flyout: BubbleBarFlyoutView? = null
@@ -86,9 +88,10 @@
private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) {
val flyout = this.flyout ?: return
val startValue = getCurrentAnimatedValueIfRunning() ?: 0f
- val duration = (ANIMATION_DURATION_MS * (1f - startValue)).toLong()
+ val duration = (EXPAND_ANIMATION_DURATION_MS * (1f - startValue)).toLong()
animator?.cancel()
val animator = ValueAnimator.ofFloat(startValue, 1f).setDuration(duration)
+ animator.interpolator = InterpolatorsAndroidX.EMPHASIZED
this.animator = animator
when (animationType) {
AnimationType.FADE ->
@@ -111,6 +114,7 @@
fun updateFlyoutFullyExpanded(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
val flyout = flyout ?: return
hideFlyout(AnimationType.FADE) {
+ callbacks.resetTopBoundary()
flyout.updateData(message) { showFlyout(AnimationType.FADE, onEnd) }
}
}
@@ -152,9 +156,10 @@
private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) {
val flyout = this.flyout ?: return
val startValue = getCurrentAnimatedValueIfRunning() ?: 1f
- val duration = (ANIMATION_DURATION_MS * startValue).toLong()
+ val duration = (COLLAPSE_ANIMATION_DURATION_MS * startValue).toLong()
animator?.cancel()
val animator = ValueAnimator.ofFloat(startValue, 0f).setDuration(duration)
+ animator.interpolator = InterpolatorsAndroidX.EMPHASIZED
this.animator = animator
when (animationType) {
AnimationType.FADE ->
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 418675c..f9f5a15 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -35,6 +35,7 @@
import androidx.core.animation.ArgbEvaluator
import com.android.launcher3.R
import com.android.launcher3.popup.RoundedArrowDrawable
+import kotlin.math.min
/** The flyout view used to notify the user of a new bubble notification. */
class BubbleBarFlyoutView(
@@ -46,6 +47,8 @@
private companion object {
// the minimum progress of the expansion animation before the content starts fading in.
const val MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA = 0.75f
+ // the rate multiple for the background color animation relative to the morph animation.
+ const val BACKGROUND_COLOR_CHANGE_RATE = 5
}
private val scheduler: FlyoutScheduler = scheduler ?: HandlerScheduler(this)
@@ -204,6 +207,8 @@
minExpansionProgressForTriangle =
positioner.distanceToRevealTriangle / translationToCollapsedPosition.y
+ backgroundPaint.color = collapsedColor
+
// post the request to start the expand animation to the looper so the view can measure
// itself
scheduler.runAfterLayout(expandAnimation)
@@ -307,8 +312,16 @@
height.toFloat() - triangleHeight + triangleOverlap,
)
+ // transform the flyout color between the collapsed and expanded states. the color
+ // transformation completes at a faster rate (BACKGROUND_COLOR_CHANGE_RATE) than the
+ // expansion animation. this helps make the color change smooth.
backgroundPaint.color =
- ArgbEvaluator.getInstance().evaluate(expansionProgress, collapsedColor, backgroundColor)
+ ArgbEvaluator.getInstance()
+ .evaluate(
+ min(expansionProgress * BACKGROUND_COLOR_CHANGE_RATE, 1f),
+ collapsedColor,
+ backgroundColor,
+ )
canvas.save()
canvas.translate(backgroundRectTx, backgroundRectTy)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
index 9a68335..45f5568 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
@@ -54,7 +54,9 @@
if (field == state) return
val transitionFromHome = field == BubbleLauncherState.HOME
field = state
- if (!bubbleBarViewController.hasBubbles()) {
+ val hasBubbles = bubbleBarViewController.hasBubbles()
+ bubbleBarViewController.onBubbleBarConfigurationChanged(hasBubbles)
+ if (!hasBubbles) {
// if there are no bubbles, there's nothing to show, so just return.
return
}
@@ -65,7 +67,6 @@
// on home but in persistent taskbar elsewhere so the position is different.
animateBubbleBarY()
}
- bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
}
override var isSysuiLocked: Boolean = false
@@ -119,7 +120,10 @@
if (field == value) return
field = value
if (launcherState == BubbleLauncherState.HOME) {
- bubbleBarViewController.bubbleBarTranslationY.updateValue(bubbleBarTranslationY)
+ if (bubbleBarTranslationYAnimator.isAnimating) {
+ bubbleBarTranslationYAnimator.cancelAnimation()
+ }
+ bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY)
if (value == 0f || value == 1f) {
// Update insets only when we reach the end values
taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
index 71303f8..e62c0d4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -87,7 +87,9 @@
set(state) {
if (field == state) return
field = state
- if (!bubbleBarViewController.hasBubbles()) {
+ val hasBubbles = bubbleBarViewController.hasBubbles()
+ bubbleBarViewController.onBubbleBarConfigurationChanged(hasBubbles)
+ if (!hasBubbles) {
// if there are no bubbles, there's nothing to show, so just return.
return
}
@@ -103,7 +105,6 @@
// Only stash if we're in an app, otherwise we're in home or overview where we should
// be un-stashed
updateStashedAndExpandedState(field == BubbleLauncherState.IN_APP, expand = false)
- bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
}
override var isSysuiLocked: Boolean = false
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index a3953ca..2164bc2 100644
--- a/quickstep/src/com/android/quickstep/BaseContainerInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
@@ -378,6 +378,9 @@
public static void getTaskDimension(Context context, DeviceProfile dp, PointF out) {
out.x = dp.widthPx;
out.y = dp.heightPx;
+ if (dp.isTablet && !DisplayController.isTransientTaskbar(context)) {
+ out.y -= dp.taskbarHeight;
+ }
}
/**
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index fcc5121..ad5720f 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -85,6 +85,7 @@
import com.android.launcher3.Flags;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.desktop.DesktopAppLaunchTransitionManager;
import com.android.launcher3.provider.RestoreDbTask;
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.statemanager.StatefulActivity;
@@ -662,6 +663,7 @@
private NavigationMode mGestureStartNavMode = null;
private DesktopVisibilityController mDesktopVisibilityController;
+ private DesktopAppLaunchTransitionManager mDesktopAppLaunchTransitionManager;
@Override
public void onCreate() {
@@ -686,6 +688,9 @@
mDesktopVisibilityController = new DesktopVisibilityController(this);
mTaskbarManager = new TaskbarManager(
this, mAllAppsActionManager, mNavCallbacks, mDesktopVisibilityController);
+ mDesktopAppLaunchTransitionManager =
+ new DesktopAppLaunchTransitionManager(this, SystemUiProxy.INSTANCE.get(this));
+ mDesktopAppLaunchTransitionManager.registerTransitions();
if (Flags.enableLauncherOverviewInWindow() || Flags.enableFallbackOverviewInWindow()) {
mRecentsWindowManager = new RecentsWindowManager(this);
}
@@ -851,6 +856,10 @@
if (mRecentsWindowManager != null) {
mRecentsWindowManager.destroy();
}
+ if (mDesktopAppLaunchTransitionManager != null) {
+ mDesktopAppLaunchTransitionManager.unregisterTransitions();
+ }
+ mDesktopAppLaunchTransitionManager = null;
mDesktopVisibilityController.onDestroy();
sConnected = false;
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 275af00..6c627ef 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -73,26 +73,29 @@
getTaskDataById(taskId).map { it?.thumbnail }.distinctUntilChangedBy { it?.snapshotId }
override fun setVisibleTasks(visibleTaskIdList: Set<Int>) {
- // Remove tasks are no longer visible
val tasksNoLongerVisible = taskRequests.keys.subtract(visibleTaskIdList)
+ val newlyVisibleTasks = visibleTaskIdList.subtract(taskRequests.keys)
+ if (tasksNoLongerVisible.isNotEmpty() || newlyVisibleTasks.isNotEmpty()) {
+ Log.d(
+ TAG,
+ "setVisibleTasks to: $visibleTaskIdList, " +
+ "removed: $tasksNoLongerVisible, added: $newlyVisibleTasks",
+ )
+ }
+
+ // Remove tasks are no longer visible
removeTasks(tasksNoLongerVisible)
// Add new tasks to be requested
- val newlyVisibleTasks = visibleTaskIdList.subtract(taskRequests.keys)
newlyVisibleTasks.forEach { taskId -> requestTaskData(taskId) }
-
- if (tasksNoLongerVisible.isNotEmpty() || newlyVisibleTasks.isNotEmpty()) {
- Log.d(TAG, "setVisibleTasks to: $visibleTaskIdList, " +
- "removed: $tasksNoLongerVisible, added: $newlyVisibleTasks")
- }
}
private fun requestTaskData(taskId: Int) {
- Log.i(TAG, "requestTaskData: $taskId")
val task = tasks.value[taskId] ?: return
taskRequests[taskId] =
Pair(
task.key,
recentsCoroutineScope.launch {
+ Log.i(TAG, "requestTaskData: $taskId")
fetchIcon(task)
fetchThumbnail(task)
},
@@ -102,8 +105,8 @@
private fun removeTasks(tasksToRemove: Set<Int>) {
if (tasksToRemove.isEmpty()) return
+ Log.i(TAG, "removeTasks: $tasksToRemove")
tasksToRemove.forEach { taskId ->
- Log.i(TAG, "removeTask: $taskId")
val request = taskRequests.remove(taskId) ?: return
val (taskKey, job) = request
job.cancel()
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index f2b9976..dd11d48 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.util.Log
import android.view.View
+import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.launcher3.util.coroutines.ProductionDispatchers
import com.android.quickstep.RecentsModel
import com.android.quickstep.recents.data.RecentTasksRepository
@@ -63,6 +64,7 @@
val recentsCoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("RecentsView"))
set(CoroutineScope::class.java.simpleName, recentsCoroutineScope)
+ set(DispatcherProvider::class.java.simpleName, ProductionDispatchers)
val recentsModel = RecentsModel.INSTANCE.get(appContext)
val taskVisualsChangedDelegate =
TaskVisualsChangedDelegateImpl(
@@ -196,6 +198,7 @@
recentsViewData = inject(),
taskViewData = inject(scopeId, extras),
taskContainerData = inject(scopeId),
+ dispatcherProvider = inject(),
getThumbnailPositionUseCase = inject(),
tasksRepository = inject(),
splashAlphaUseCase = inject(scopeId),
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index eb9c047..a8c8659 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -33,7 +33,6 @@
import com.android.launcher3.util.ViewPool
import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.recents.di.get
-import com.android.quickstep.recents.di.inject
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
@@ -43,7 +42,6 @@
import com.android.quickstep.util.TaskCornerRadius
import com.android.quickstep.views.FixedSizeImageView
import com.android.systemui.shared.system.QuickStepContract
-import kotlin.math.abs
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -53,8 +51,7 @@
import kotlinx.coroutines.flow.onEach
class TaskThumbnailView : ConstraintLayout, ViewPool.Reusable {
-
- private val viewData: TaskThumbnailViewData by RecentsDependencies.inject(this)
+ private lateinit var viewData: TaskThumbnailViewData
private lateinit var viewModel: TaskThumbnailViewModel
private lateinit var viewAttachedScope: CoroutineScope
@@ -92,10 +89,12 @@
super.onAttachedToWindow()
viewAttachedScope =
CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskThumbnailView"))
+ viewData = RecentsDependencies.get(this)
+ updateViewDataValues()
viewModel = RecentsDependencies.get(this)
viewModel.uiState
.onEach { viewModelUiState ->
- Log.d(TAG, "viewModelUiState changed from $uiState to: $viewModelUiState")
+ Log.d(TAG, "viewModelUiState changed from: $uiState to: $viewModelUiState")
uiState = viewModelUiState
resetViews()
when (viewModelUiState) {
@@ -144,11 +143,15 @@
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (changed) {
- viewData.width.value = abs(right - left)
- viewData.height.value = abs(bottom - top)
+ updateViewDataValues()
}
}
+ private fun updateViewDataValues() {
+ viewData.width.value = width
+ viewData.height.value = height
+ }
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (uiState is SnapshotSplash) {
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
index c82ed9a..203177a 100644
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -85,14 +85,18 @@
}
private fun initOverlay(enabledState: Enabled) {
- Log.d(TAG, "initOverlay - taskId: ${task.key.id}, thumbnail: ${enabledState.thumbnail}")
+ if (DEBUG) {
+ Log.d(TAG, "initOverlay - taskId: ${task.key.id}, thumbnail: ${enabledState.thumbnail}")
+ }
with(getThumbnailPositionState()) {
overlay.initOverlay(task, enabledState.thumbnail, matrix, isRotated)
}
}
private fun reset() {
- Log.d(TAG, "reset - taskId: ${task.key.id}")
+ if (DEBUG) {
+ Log.d(TAG, "reset - taskId: ${task.key.id}")
+ }
overlay.reset()
}
@@ -105,5 +109,6 @@
companion object {
private const val TAG = "TaskOverlayHelper"
+ private const val DEBUG = false
}
}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
index bd47cec..8b15a82 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -21,6 +21,7 @@
import android.graphics.Matrix
import android.util.Log
import androidx.core.graphics.ColorUtils
+import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.data.RecentTasksRepository
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState
@@ -42,6 +43,7 @@
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
@@ -50,6 +52,7 @@
recentsViewData: RecentsViewData,
taskViewData: TaskViewData,
taskContainerData: TaskContainerData,
+ dispatcherProvider: DispatcherProvider,
private val tasksRepository: RecentTasksRepository,
private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
private val splashAlphaUseCase: SplashAlphaUseCase,
@@ -73,6 +76,7 @@
tintAmount ->
max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
}
+
override val splashAlpha = splashProgress.flatMapLatest { it }
private val isLiveTile =
@@ -84,6 +88,7 @@
runningTaskIds.contains(taskId) && !runningTaskShowScreenshot
}
.distinctUntilChanged()
+ .flowOn(dispatcherProvider.default)
override val uiState: Flow<TaskThumbnailUiState> =
combine(task.flatMapLatest { it }, isLiveTile) { taskVal, isRunning ->
@@ -105,6 +110,7 @@
}
}
.distinctUntilChanged()
+ .flowOn(dispatcherProvider.default)
override fun bind(taskId: Int) {
Log.d(TAG, "bind taskId: $taskId")
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index ea582c4..d35a36a 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -163,6 +163,8 @@
*/
private Pair<InstanceId, com.android.launcher3.logging.InstanceId> mSessionInstanceIds;
+ private boolean mIsDestroyed = false;
+
private final BackPressHandler mSplitBackHandler = new BackPressHandler() {
@Override
public boolean canHandleBack() {
@@ -199,6 +201,7 @@
public void onDestroy() {
mContainer = null;
+ mIsDestroyed = true;
mActivityBackCallback = null;
mAppPairsController.onDestroy();
mSplitSelectDataHolder.onDestroy();
@@ -744,7 +747,9 @@
*/
public void resetState() {
mSplitSelectDataHolder.resetState();
- mContainer.<RecentsView>getOverviewPanel().resetDesktopTaskFromSplitSelectState();
+ if (!mIsDestroyed) {
+ mContainer.<RecentsView>getOverviewPanel().resetDesktopTaskFromSplitSelectState();
+ }
dispatchOnSplitSelectionExit();
mRecentsAnimationRunning = false;
mLaunchingTaskView = null;
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 839c42e..3a4e328 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -3694,31 +3694,43 @@
int currentPageScroll = getScrollForPage(mCurrentPage);
int lastGridTaskScroll = getScrollForPage(indexOfChild(lastGridTaskView));
boolean currentPageSnapsToEndOfGrid = currentPageScroll == lastGridTaskScroll;
+
+ int topGridRowSize = mTopRowIdSet.size();
+ int numLargeTiles = mUtils.getLargeTileCount(getTaskViews());
+ int bottomGridRowSize = taskCount - mTopRowIdSet.size() - numLargeTiles;
+ boolean topRowLonger = topGridRowSize > bottomGridRowSize;
+ boolean bottomRowLonger = bottomGridRowSize > topGridRowSize;
+ boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId);
+ boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed;
+ if (dismissedTaskFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) {
+ topGridRowSize--;
+ }
+ if (dismissedTaskFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) {
+ bottomGridRowSize--;
+ }
+ int longRowWidth = Math.max(topGridRowSize, bottomGridRowSize)
+ * (mLastComputedGridTaskSize.width() + mPageSpacing);
+ if (!enableGridOnlyOverview() && !isStagingFocusedTask) {
+ longRowWidth += mLastComputedTaskSize.width() + mPageSpacing;
+ }
+ // Compensate the removed gap if we don't already have shortTotalCompensation,
+ // and adjust accordingly to the new shortTotalCompensation after dismiss.
+ int newClearAllShortTotalWidthTranslation = 0;
+ if (mClearAllShortTotalWidthTranslation == 0) {
+ // If first task is not in the expected position (mLastComputedTaskSize) and being too
+ // close to ClearAllButton, then apply extra translation to ClearAllButton.
+ int firstTaskStart = mLastComputedGridSize.left + longRowWidth;
+ int expectedFirstTaskStart = mLastComputedTaskSize.right;
+ if (firstTaskStart < expectedFirstTaskStart) {
+ newClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart;
+ }
+ }
if (lastGridTaskView != null && lastGridTaskView.isVisibleToUser()) {
// After dismissal, animate translation of the remaining tasks to fill any gap left
// between the end of the grid and the clear all button. Only animate if the clear
// all button is visible or would become visible after dismissal.
float longGridRowWidthDiff = 0;
- int topGridRowSize = mTopRowIdSet.size();
- int numLargeTiles = mUtils.getLargeTileCount(getTaskViews());
- int bottomGridRowSize = taskCount - mTopRowIdSet.size() - numLargeTiles;
- boolean topRowLonger = topGridRowSize > bottomGridRowSize;
- boolean bottomRowLonger = bottomGridRowSize > topGridRowSize;
- boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId);
- boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed;
- if (dismissedTaskFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) {
- topGridRowSize--;
- }
- if (dismissedTaskFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) {
- bottomGridRowSize--;
- }
- int longRowWidth = Math.max(topGridRowSize, bottomGridRowSize)
- * (mLastComputedGridTaskSize.width() + mPageSpacing);
- if (!enableGridOnlyOverview() && !isStagingFocusedTask) {
- longRowWidth += mLastComputedTaskSize.width() + mPageSpacing;
- }
-
float gapWidth = 0;
if ((topRowLonger && dismissedTaskFromTop)
|| (bottomRowLonger && dismissedTaskFromBottom)) {
@@ -3730,17 +3742,6 @@
}
if (gapWidth > 0) {
if (mClearAllShortTotalWidthTranslation == 0) {
- // Compensate the removed gap if we don't already have shortTotalCompensation,
- // and adjust accordingly to the new shortTotalCompensation after dismiss.
- int newClearAllShortTotalWidthTranslation = 0;
- if (longRowWidth < mLastComputedGridSize.width()) {
- DeviceProfile deviceProfile = mContainer.getDeviceProfile();
- newClearAllShortTotalWidthTranslation =
- (mIsRtl
- ? mLastComputedTaskSize.right
- : deviceProfile.widthPx - mLastComputedTaskSize.left)
- - longRowWidth - deviceProfile.overviewGridSideMargin;
- }
float gapCompensation = gapWidth - newClearAllShortTotalWidthTranslation;
longGridRowWidthDiff += mIsRtl ? -gapCompensation : gapCompensation;
}
@@ -3830,6 +3831,8 @@
: mUtils.getDesktopTaskViewCount(getTaskViews());
stagingTranslation = getPagedOrientationHandler().getPrimaryScroll(this)
- getScrollForPage(nextSnappedPage);
+ stagingTranslation += mIsRtl ? newClearAllShortTotalWidthTranslation
+ : -newClearAllShortTotalWidthTranslation;
}
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
@@ -3888,7 +3891,7 @@
anim.setFloat(taskView, TaskView.DISMISS_SCALE, scale,
clampToProgress(LINEAR, animationStartProgress,
dismissTranslationInterpolationEnd));
- primaryTranslation += mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth;
+ primaryTranslation += dismissedTaskWidth;
animationEndProgress = dismissTranslationInterpolationEnd;
float secondaryTranslation = -mTaskGridVerticalDiff;
if (!nextFocusedTaskFromTop) {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
index f22c672..3616fbb 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
@@ -25,6 +25,7 @@
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/** Helper for [RecentsView] to interact with the [RecentsViewModel]. */
class RecentsViewModelHelper(private val recentsViewModel: RecentsViewModel) {
@@ -32,7 +33,7 @@
fun onAttachedToWindow() {
viewAttachedScope =
- CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("RecentsView"))
+ CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineName("RecentsView"))
}
fun onDetachedFromWindow() {
@@ -50,7 +51,7 @@
viewAttachedScope.launch {
recentsViewModel.waitForRunningTaskShowScreenshotToUpdate()
recentsViewModel.waitForThumbnailsToUpdate(updatedThumbnails)
- ViewUtils.postFrameDrawn(taskView, onFinishRunnable)
+ withContext(Dispatchers.Main) { ViewUtils.postFrameDrawn(taskView, onFinishRunnable) }
}
}
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 25aba39..c940fb4 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -110,7 +110,6 @@
return snapshotView as TaskThumbnailViewDeprecated
}
- // TODO(b/334826842): Support shouldShowSplashView for new TTV.
val shouldShowSplashView: Boolean
get() =
if (enableRefactorTaskThumbnail())
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 0760618..b1cb407 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -1221,8 +1221,6 @@
if (isQuickSwitch) {
setFreezeRecentTasksReordering()
}
- // TODO(b/334826842) no work required - add splash functionality to new TTV -
- // cold start e.g. restart device. Small splash moving to bigger splash
disableStartingWindow = firstContainer.shouldShowSplashView
}
Executors.UI_HELPER_EXECUTOR.execute {
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 48f3fc2..582ea54 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
@@ -1123,7 +1123,7 @@
animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
// the flyout should now reverse and expand
- animatorTestRule.advanceTimeBy(100)
+ animatorTestRule.advanceTimeBy(400)
}
assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
@@ -1362,21 +1362,21 @@
private fun waitForFlyoutToShow() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(250)
+ animatorTestRule.advanceTimeBy(400)
}
assertThat(flyoutView).isNotNull()
}
private fun waitForFlyoutToHide() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(250)
+ animatorTestRule.advanceTimeBy(350)
}
assertThat(flyoutView).isNull()
}
private fun waitForFlyoutToFadeOutAndBackIn() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(500)
+ animatorTestRule.advanceTimeBy(750)
}
assertThat(flyoutView).isNotNull()
}
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 2997ac9..103c769 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
@@ -50,6 +50,9 @@
private var onLeft = true
private var flyoutTy = 50f
+ private val showAnimationDuration = 400L
+ private val hideAnimationDuration = 350L
+
@Before
fun setUp() {
flyoutContainer = FrameLayout(context)
@@ -118,7 +121,7 @@
assertThat(flyoutController.hasFlyout()).isTrue()
assertThat(flyoutContainer.childCount).isEqualTo(1)
flyoutController.collapseFlyout {}
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(hideAnimationDuration)
}
assertThat(flyoutContainer.childCount).isEqualTo(0)
assertThat(flyoutController.hasFlyout()).isFalse()
@@ -135,7 +138,7 @@
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
}
assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
}
@@ -148,7 +151,7 @@
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
}
assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(0)
}
@@ -159,7 +162,7 @@
setupAndShowFlyout()
assertThat(flyoutContainer.childCount).isEqualTo(1)
flyoutController.collapseFlyout {}
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(hideAnimationDuration)
}
assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
}
@@ -172,7 +175,7 @@
val flyoutView = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
assertThat(flyoutView.alpha).isEqualTo(1f)
flyoutController.cancelFlyout {}
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(hideAnimationDuration)
assertThat(flyoutView.alpha).isEqualTo(0f)
}
assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
@@ -185,7 +188,7 @@
assertThat(flyoutContainer.childCount).isEqualTo(1)
val flyoutView = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
assertThat(flyoutView.alpha).isEqualTo(1f)
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
flyoutView.performClick()
}
assertThat(flyoutCallbacks.flyoutClicked).isTrue()
@@ -221,7 +224,7 @@
fun updateFlyoutFullyExpanded() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
setupAndShowFlyout()
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
}
assertThat(flyoutController.hasFlyout()).isTrue()
@@ -234,13 +237,13 @@
flyoutController.updateFlyoutFullyExpanded(newFlyoutMessage) {}
// advance the timer so that the fade out animation plays
- animatorTestRule.advanceTimeBy(250)
+ animatorTestRule.advanceTimeBy(hideAnimationDuration)
assertThat(flyout.alpha).isEqualTo(0)
assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
.isEqualTo("new message")
// advance the timer so that the fade in animation plays
- animatorTestRule.advanceTimeBy(250)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
assertThat(flyout.alpha).isEqualTo(1)
}
assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
@@ -250,7 +253,7 @@
fun updateFlyoutWhileCollapsing() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
setupAndShowFlyout()
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
}
assertThat(flyoutController.hasFlyout()).isTrue()
@@ -265,9 +268,10 @@
var flyoutReversed = false
flyoutController.updateFlyoutWhileCollapsing(newFlyoutMessage) { flyoutReversed = true }
- // the collapse animation ran for 125ms when it was updated, so reversing it should only
- // run for the same amount of time
- animatorTestRule.advanceTimeBy(125)
+ // the collapse and expand animations use an emphasized interpolator, so the reverse
+ // path does not take the same time. advance the timer the by full duration of the show
+ // animation to ensure it completes
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
assertThat(flyout.alpha).isEqualTo(1)
assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
index e3d41e7..f795ab1 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
@@ -86,6 +86,20 @@
}
@Test
+ fun updateLauncherState_noBubbles_controllerNotified() {
+ // Given bubble bar has no bubbles
+ whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+
+ // When switch to home screen
+ getInstrumentation().runOnMainSync {
+ persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+ }
+
+ // Then bubble bar view controller is notified
+ verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ false)
+ }
+
+ @Test
fun setBubblesShowingOnHomeUpdatedToFalse_barPositionYUpdated_controllersNotified() {
// Given bubble bar is on home and has bubbles
whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
@@ -284,6 +298,21 @@
}
@Test
+ fun inAppDisplayOverrideProgress_onHome_cancelExistingAnimation() {
+ whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+ persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+
+ bubbleBarViewController.bubbleBarTranslationY.animateToValue(100f)
+ advanceTimeBy(10)
+ assertThat(bubbleBarViewController.bubbleBarTranslationY.isAnimating).isTrue()
+
+ getInstrumentation().runOnMainSync {
+ persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+ }
+ assertThat(bubbleBarViewController.bubbleBarTranslationY.isAnimating).isFalse()
+ }
+
+ @Test
fun inAppDisplayProgressUpdate_inApp_noTranslationUpdate() {
whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
index 64416dd..1bbd12a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
@@ -119,6 +119,20 @@
}
@Test
+ fun updateLauncherState_noBubbles_controllerNotified() {
+ // Given bubble bar has no bubbles
+ whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+
+ // When switch to home screen
+ getInstrumentation().runOnMainSync {
+ mTransientBubbleStashController.launcherState = BubbleLauncherState.HOME
+ }
+
+ // Then bubble bar view controller is notified
+ verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ false)
+ }
+
+ @Test
fun setBubblesShowingOnHomeUpdatedToTrue_barPositionYUpdated_controllersNotified() {
// Given bubble bar is on home and has bubbles
whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
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 c541d3d..e3a6adf 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
@@ -24,6 +24,7 @@
import android.graphics.drawable.Drawable
import android.view.Surface
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.util.TestDispatcherProvider
import com.android.quickstep.recents.data.FakeTasksRepository
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
@@ -42,6 +43,8 @@
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -51,18 +54,24 @@
/** Test for [TaskThumbnailView] */
@RunWith(AndroidJUnit4::class)
class TaskThumbnailViewModelImplTest {
+ private val dispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(dispatcher)
+
private var taskViewType = TaskViewType.SINGLE
private val recentsViewData = RecentsViewData()
private val taskViewData by lazy { TaskViewData(taskViewType) }
private val taskContainerData = TaskContainerData()
+ private val dispatcherProvider = TestDispatcherProvider(dispatcher)
private val tasksRepository = FakeTasksRepository()
private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
private val splashAlphaUseCase: SplashAlphaUseCase = mock()
+
private val systemUnderTest by lazy {
TaskThumbnailViewModelImpl(
recentsViewData,
taskViewData,
taskContainerData,
+ dispatcherProvider,
tasksRepository,
mGetThumbnailPositionUseCase,
splashAlphaUseCase,
@@ -72,81 +81,85 @@
private val tasks = (0..5).map(::createTaskWithId)
@Test
- fun initialStateIsUninitialized() = runTest {
- assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
- }
+ fun initialStateIsUninitialized() =
+ testScope.runTest { assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized) }
@Test
- fun bindRunningTask_thenStateIs_LiveTile() = runTest {
- val taskId = 1
- tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(setOf(taskId))
- recentsViewData.runningTaskIds.value = setOf(taskId)
- systemUnderTest.bind(taskId)
+ fun bindRunningTask_thenStateIs_LiveTile() =
+ testScope.runTest {
+ val taskId = 1
+ tasksRepository.seedTasks(tasks)
+ tasksRepository.setVisibleTasks(setOf(taskId))
+ recentsViewData.runningTaskIds.value = setOf(taskId)
+ systemUnderTest.bind(taskId)
- assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
- }
+ assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
+ }
@Test
- fun bindRunningTaskShouldShowScreenshot_thenStateIs_SnapshotSplash() = runTest {
- val taskId = 1
- val expectedThumbnailData = createThumbnailData()
- tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
- val expectedIconData = mock<Drawable>()
- tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
- tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(setOf(taskId))
- recentsViewData.runningTaskIds.value = setOf(taskId)
- recentsViewData.runningTaskShowScreenshot.value = true
- systemUnderTest.bind(taskId)
+ fun bindRunningTaskShouldShowScreenshot_thenStateIs_SnapshotSplash() =
+ testScope.runTest {
+ val taskId = 1
+ val expectedThumbnailData = createThumbnailData()
+ tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
+ val expectedIconData = mock<Drawable>()
+ tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
+ tasksRepository.seedTasks(tasks)
+ tasksRepository.setVisibleTasks(setOf(taskId))
+ recentsViewData.runningTaskIds.value = setOf(taskId)
+ recentsViewData.runningTaskShowScreenshot.value = true
+ systemUnderTest.bind(taskId)
- assertThat(systemUnderTest.uiState.first())
- .isEqualTo(
- SnapshotSplash(
- Snapshot(
- backgroundColor = Color.rgb(1, 1, 1),
- bitmap = expectedThumbnailData.thumbnail!!,
- thumbnailRotation = Surface.ROTATION_0,
- ),
- expectedIconData,
+ assertThat(systemUnderTest.uiState.first())
+ .isEqualTo(
+ SnapshotSplash(
+ Snapshot(
+ backgroundColor = Color.rgb(1, 1, 1),
+ bitmap = expectedThumbnailData.thumbnail!!,
+ thumbnailRotation = Surface.ROTATION_0,
+ ),
+ expectedIconData,
+ )
)
- )
- }
+ }
@Test
- fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsPassedThrough() = runTest {
- recentsViewData.fullscreenProgress.value = 0.5f
+ fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsPassedThrough() =
+ testScope.runTest {
+ recentsViewData.fullscreenProgress.value = 0.5f
- assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.5f)
+ assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.5f)
- recentsViewData.fullscreenProgress.value = 0.6f
+ recentsViewData.fullscreenProgress.value = 0.6f
- assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.6f)
- }
+ assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.6f)
+ }
@Test
- fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsConstantForDesktop() = runTest {
- taskViewType = TaskViewType.DESKTOP
- recentsViewData.fullscreenProgress.value = 0.5f
+ fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsConstantForDesktop() =
+ testScope.runTest {
+ taskViewType = TaskViewType.DESKTOP
+ recentsViewData.fullscreenProgress.value = 0.5f
- assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
+ assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
- recentsViewData.fullscreenProgress.value = 0.6f
+ recentsViewData.fullscreenProgress.value = 0.6f
- assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
- }
+ assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
+ }
@Test
- fun setAncestorScales_thenScaleIsCalculated() = runTest {
- recentsViewData.scale.value = 0.5f
- taskViewData.scale.value = 0.6f
+ fun setAncestorScales_thenScaleIsCalculated() =
+ testScope.runTest {
+ recentsViewData.scale.value = 0.5f
+ taskViewData.scale.value = 0.6f
- assertThat(systemUnderTest.inheritedScale.first()).isEqualTo(0.3f)
- }
+ assertThat(systemUnderTest.inheritedScale.first()).isEqualTo(0.3f)
+ }
@Test
fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() =
- runTest {
+ testScope.runTest {
val runningTaskId = 1
val stoppedTaskId = 2
tasksRepository.seedTasks(tasks)
@@ -161,125 +174,138 @@
}
@Test
- fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() = runTest {
- val stoppedTaskId = 2
- tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(setOf(stoppedTaskId))
+ fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() =
+ testScope.runTest {
+ val stoppedTaskId = 2
+ tasksRepository.seedTasks(tasks)
+ tasksRepository.setVisibleTasks(setOf(stoppedTaskId))
- systemUnderTest.bind(stoppedTaskId)
- assertThat(systemUnderTest.uiState.first())
- .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
- }
+ systemUnderTest.bind(stoppedTaskId)
+ assertThat(systemUnderTest.uiState.first())
+ .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+ }
@Test
- fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() = runTest {
- val taskId = 2
- tasksRepository.seedThumbnailData(mapOf(taskId to createThumbnailData()))
- tasks[taskId].isLocked = true
- tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(setOf(taskId))
+ fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() =
+ testScope.runTest {
+ val taskId = 2
+ tasksRepository.seedThumbnailData(mapOf(taskId to createThumbnailData()))
+ tasks[taskId].isLocked = true
+ tasksRepository.seedTasks(tasks)
+ tasksRepository.setVisibleTasks(setOf(taskId))
- systemUnderTest.bind(taskId)
- assertThat(systemUnderTest.uiState.first())
- .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
- }
+ systemUnderTest.bind(taskId)
+ assertThat(systemUnderTest.uiState.first())
+ .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+ }
@Test
- fun bindStoppedTaskWithThumbnail_thenStateIs_SnapshotSplash_withAlphaRemoved() = runTest {
- val taskId = 2
- val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
- tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
- val expectedIconData = mock<Drawable>()
- tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
- tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(setOf(taskId))
+ fun bindStoppedTaskWithThumbnail_thenStateIs_SnapshotSplash_withAlphaRemoved() =
+ testScope.runTest {
+ val taskId = 2
+ val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
+ tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
+ val expectedIconData = mock<Drawable>()
+ tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
+ tasksRepository.seedTasks(tasks)
+ tasksRepository.setVisibleTasks(setOf(taskId))
- systemUnderTest.bind(taskId)
- assertThat(systemUnderTest.uiState.first())
- .isEqualTo(
- SnapshotSplash(
- Snapshot(
- backgroundColor = Color.rgb(2, 2, 2),
- bitmap = expectedThumbnailData.thumbnail!!,
- thumbnailRotation = Surface.ROTATION_270,
- ),
- expectedIconData,
+ systemUnderTest.bind(taskId)
+ assertThat(systemUnderTest.uiState.first())
+ .isEqualTo(
+ SnapshotSplash(
+ Snapshot(
+ backgroundColor = Color.rgb(2, 2, 2),
+ bitmap = expectedThumbnailData.thumbnail!!,
+ thumbnailRotation = Surface.ROTATION_270,
+ ),
+ expectedIconData,
+ )
)
- )
- }
+ }
@Test
- fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshotSplash() = runTest {
- val taskId = 2
- val expectedThumbnailData = createThumbnailData()
- tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
- val expectedIconData = mock<Drawable>()
- tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
- tasksRepository.seedTasks(tasks)
+ fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshotSplash() =
+ testScope.runTest {
+ val taskId = 2
+ val expectedThumbnailData = createThumbnailData()
+ tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
+ val expectedIconData = mock<Drawable>()
+ tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
+ tasksRepository.seedTasks(tasks)
- systemUnderTest.bind(taskId)
- assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
+ systemUnderTest.bind(taskId)
+ assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
- tasksRepository.setVisibleTasks(setOf(taskId))
- assertThat(systemUnderTest.uiState.first())
- .isEqualTo(
- SnapshotSplash(
- Snapshot(
- backgroundColor = Color.rgb(2, 2, 2),
- bitmap = expectedThumbnailData.thumbnail!!,
- thumbnailRotation = Surface.ROTATION_0,
- ),
- expectedIconData,
+ tasksRepository.setVisibleTasks(setOf(taskId))
+ assertThat(systemUnderTest.uiState.first())
+ .isEqualTo(
+ SnapshotSplash(
+ Snapshot(
+ backgroundColor = Color.rgb(2, 2, 2),
+ bitmap = expectedThumbnailData.thumbnail!!,
+ thumbnailRotation = Surface.ROTATION_0,
+ ),
+ expectedIconData,
+ )
)
- )
- }
+ }
@Test
- fun getSnapshotMatrix_MissingThumbnail() = runTest {
- val taskId = 2
- val isRtl = true
+ fun getSnapshotMatrix_MissingThumbnail() =
+ testScope.runTest {
+ val taskId = 2
+ val isRtl = true
- whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MissingThumbnail)
+ whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .thenReturn(MissingThumbnail)
- systemUnderTest.bind(taskId)
- assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .isEqualTo(Matrix.IDENTITY_MATRIX)
- }
+ systemUnderTest.bind(taskId)
+ assertThat(
+ systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+ )
+ .isEqualTo(Matrix.IDENTITY_MATRIX)
+ }
@Test
- fun getSnapshotMatrix_MatrixScaling() = runTest {
- val taskId = 2
- val isRtl = true
+ fun getSnapshotMatrix_MatrixScaling() =
+ testScope.runTest {
+ val taskId = 2
+ val isRtl = true
- whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MatrixScaling(MATRIX, isRotated = false))
+ whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .thenReturn(MatrixScaling(MATRIX, isRotated = false))
- systemUnderTest.bind(taskId)
- assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .isEqualTo(MATRIX)
- }
+ systemUnderTest.bind(taskId)
+ assertThat(
+ systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+ )
+ .isEqualTo(MATRIX)
+ }
@Test
- fun getForegroundScrimDimProgress_returnsForegroundMaxScrim() = runTest {
- recentsViewData.tintAmount.value = 0.32f
- taskContainerData.taskMenuOpenProgress.value = 0f
- assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.32f)
- }
+ fun getForegroundScrimDimProgress_returnsForegroundMaxScrim() =
+ testScope.runTest {
+ recentsViewData.tintAmount.value = 0.32f
+ taskContainerData.taskMenuOpenProgress.value = 0f
+ assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.32f)
+ }
@Test
- fun getTaskMenuScrimDimProgress_returnsTaskMenuScrim() = runTest {
- recentsViewData.tintAmount.value = 0f
- taskContainerData.taskMenuOpenProgress.value = 1f
- assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.4f)
- }
+ fun getTaskMenuScrimDimProgress_returnsTaskMenuScrim() =
+ testScope.runTest {
+ recentsViewData.tintAmount.value = 0f
+ taskContainerData.taskMenuOpenProgress.value = 1f
+ assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.4f)
+ }
@Test
- fun getForegroundScrimDimProgress_returnsNoScrim() = runTest {
- recentsViewData.tintAmount.value = 0f
- taskContainerData.taskMenuOpenProgress.value = 0f
- assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0f)
- }
+ fun getForegroundScrimDimProgress_returnsNoScrim() =
+ testScope.runTest {
+ recentsViewData.tintAmount.value = 0f
+ taskContainerData.taskMenuOpenProgress.value = 0f
+ assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0f)
+ }
private fun createTaskWithId(taskId: Int) =
Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
index a8f39af..2fb08dd 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
@@ -16,6 +16,8 @@
package com.android.quickstep;
+import android.util.Log;
+
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
@@ -29,6 +31,8 @@
@RunWith(AndroidJUnit4.class)
public class TaplStartLauncherViaGestureTests extends AbstractQuickStepTest {
+ public static final String TAG = "TaplStartLauncherViaGestureTests";
+
static final int STRESS_REPEAT_COUNT = 10;
private enum TestCase {
@@ -69,7 +73,9 @@
}
private void runTest(TestCase testCase) {
+ long testStartTime = System.currentTimeMillis();
for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
+ long loopStartTime = System.currentTimeMillis();
// Destroy Launcher activity.
closeLauncherActivity();
@@ -84,7 +90,10 @@
default:
throw new IllegalStateException("Cannot run test case: " + testCase);
}
+ Log.d(TAG, "Loop " + (i + 1) + " runtime="
+ + (System.currentTimeMillis() - loopStartTime) + "ms");
}
+ Log.d(TAG, "Test runtime=" + (System.currentTimeMillis() - testStartTime) + "ms");
switch (testCase) {
case TO_OVERVIEW:
closeLauncherActivity();
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
new file mode 100644
index 0000000..26189df
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.desktop
+
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionFilter
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.quickstep.SystemUiProxy
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DesktopAppLaunchTransitionManagerTest {
+
+ @get:Rule val mSetFlagsRule = SetFlagsRule()
+
+ private val mockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .mockStatic(DesktopModeStatus::class.java)
+ .startMocking()
+
+ private val context = mock<Context>()
+ private val systemUiProxy = mock<SystemUiProxy>()
+ private lateinit var transitionManager: DesktopAppLaunchTransitionManager
+
+ @Before
+ fun setUp() {
+ whenever(context.resources).thenReturn(mock())
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+ transitionManager = DesktopAppLaunchTransitionManager(context, systemUiProxy)
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS)
+ fun registerTransitions_appLaunchFlagEnabled_registersTransition() {
+ transitionManager.registerTransitions()
+
+ verify(systemUiProxy, times(1)).registerRemoteTransition(any(), any())
+ }
+
+ @Test
+ @DisableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS)
+ fun registerTransitions_appLaunchFlagDisabled_doesntRegisterTransition() {
+ transitionManager.registerTransitions()
+
+ verify(systemUiProxy, times(0)).registerRemoteTransition(any(), any())
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS)
+ fun registerTransitions_usesCorrectFilter() {
+ transitionManager.registerTransitions()
+ val filterArgumentCaptor = argumentCaptor<TransitionFilter>()
+
+ verify(systemUiProxy, times(1))
+ .registerRemoteTransition(any(), filterArgumentCaptor.capture())
+
+ assertThat(filterArgumentCaptor.lastValue).isNotNull()
+ assertThat(filterArgumentCaptor.lastValue.mTypeSet)
+ .isEqualTo(intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT))
+ assertThat(filterArgumentCaptor.lastValue.mRequirements).hasLength(1)
+ val launchRequirement = filterArgumentCaptor.lastValue.mRequirements!![0]
+ assertThat(launchRequirement.mModes).isEqualTo(intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT))
+ assertThat(launchRequirement.mActivityType).isEqualTo(ACTIVITY_TYPE_STANDARD)
+ assertThat(launchRequirement.mWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+}
diff --git a/res/layout/widgets_list_expand_button.xml b/res/layout/widgets_list_expand_button.xml
index 17c19ac..ff2d777 100644
--- a/res/layout/widgets_list_expand_button.xml
+++ b/res/layout/widgets_list_expand_button.xml
@@ -15,6 +15,7 @@
-->
<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/widget_list_expand_button"
style="@style/Button.Rounded.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
index 46f2d8a..e2f0e09 100644
--- a/res/layout/work_mode_fab.xml
+++ b/res/layout/work_mode_fab.xml
@@ -17,6 +17,7 @@
android:id="@+id/work_mode_toggle"
android:layout_height="@dimen/work_fab_height"
android:layout_width="wrap_content"
+ android:elevation="@dimen/work_fab_elevation"
android:minHeight="@dimen/work_fab_height"
android:gravity="center_vertical"
android:background="@drawable/work_mode_fab_background"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c69778a..61d99d7 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -155,6 +155,7 @@
<!-- Floating action button inside work tab to toggle work profile -->
<dimen name="work_fab_height">56dp</dimen>
<dimen name="work_fab_radius">16dp</dimen>
+ <dimen name="work_fab_elevation">6dp</dimen>
<dimen name="work_fab_icon_size">24dp</dimen>
<dimen name="work_fab_icon_vertical_margin">16dp</dimen>
<dimen name="work_fab_icon_start_margin_expanded">4dp</dimen>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 34cf56b..ef5c88a 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -20,6 +20,7 @@
import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
import static android.text.Layout.Alignment.ALIGN_NORMAL;
+import static com.android.launcher3.Flags.enableContrastTiles;
import static com.android.launcher3.Flags.enableCursorHoverStates;
import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
@@ -39,6 +40,7 @@
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.icu.text.MessageFormat;
@@ -722,6 +724,29 @@
}
}
+ /** Draws a background behind the App Title label when required. **/
+ public void drawAppContrastTile(Canvas canvas) {
+ RectF appTitleBounds;
+ Paint.FontMetrics fm = getPaint().getFontMetrics();
+ Rect tmpRect = new Rect();
+ getDrawingRect(tmpRect);
+
+ if (mIcon == null) {
+ appTitleBounds = new RectF(0, 0, tmpRect.right,
+ (int) Math.ceil(fm.bottom - fm.top));
+ } else {
+ Rect iconBounds = new Rect();
+ getIconBounds(iconBounds);
+ int textStart = iconBounds.bottom + getCompoundDrawablePadding();
+ appTitleBounds = new RectF(tmpRect.left, textStart, tmpRect.right,
+ textStart + (int) Math.ceil(fm.bottom - fm.top));
+ }
+
+ canvas.drawRoundRect(appTitleBounds, appTitleBounds.height() / 2,
+ appTitleBounds.height() / 2,
+ PillColorProvider.getInstance(getContext()).getAppTitlePillPaint());
+ }
+
/** Draws a line under the app icon if this is representing a running app in Desktop Mode. */
protected void drawRunningAppIndicatorIfNecessary(Canvas canvas) {
if (mRunningAppState == RunningAppState.NOT_RUNNING || mDisplay != DISPLAY_TASKBAR) {
@@ -909,7 +934,9 @@
@Override
public void setTextColor(ColorStateList colors) {
- mTextColor = colors.getDefaultColor();
+ mTextColor = shouldDrawAppContrastTile() ? PillColorProvider.getInstance(
+ getContext()).getAppTitleTextPaint().getColor()
+ : colors.getDefaultColor();
mTextColorStateList = colors;
if (Float.compare(mTextAlpha, 1) == 0) {
super.setTextColor(colors);
@@ -926,6 +953,15 @@
&& info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
}
+ /**
+ * Whether or not an App title contrast tile should be drawn for this element.
+ **/
+ public boolean shouldDrawAppContrastTile() {
+ return mDisplay == DISPLAY_WORKSPACE && shouldTextBeVisible()
+ && PillColorProvider.getInstance(getContext()).isMatchaEnabled()
+ && enableContrastTiles();
+ }
+
public void setTextVisibility(boolean visible) {
setTextAlpha(visible ? 1 : 0);
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 6145077..305941e 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -537,6 +537,7 @@
mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
mWidgetPickerDataProvider = new WidgetPickerDataProvider();
+ PillColorProvider.getInstance(mWorkspace.getContext()).registerObserver();
boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this);
if (internalStateHandled) {
@@ -1813,6 +1814,7 @@
// changes while launcher is still loading.
getRootView().getViewTreeObserver().removeOnPreDrawListener(mOnInitialBindListener);
mOverlayManager.onActivityDestroyed();
+ PillColorProvider.getInstance(mWorkspace.getContext()).unregisterObserver();
}
public LauncherAccessibilityDelegate getAccessibilityDelegate() {
diff --git a/src/com/android/launcher3/PillColorPorovider.kt b/src/com/android/launcher3/PillColorPorovider.kt
new file mode 100644
index 0000000..347c5d6
--- /dev/null
+++ b/src/com/android/launcher3/PillColorPorovider.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3
+
+import android.content.Context
+import android.database.ContentObserver
+import android.graphics.Paint
+import android.net.Uri
+import android.provider.Settings
+import com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR
+
+class PillColorProvider private constructor(c: Context) {
+ private val context = c.applicationContext
+
+ private val matchaUri by lazy { Settings.Secure.getUriFor(MATCHA_SETTING) }
+ var appTitlePillPaint = Paint()
+ private set
+
+ var appTitleTextPaint = Paint()
+ private set
+
+ private var isMatchaEnabledInternal = 0
+
+ var isMatchaEnabled = isMatchaEnabledInternal != 0
+
+ private val pillColorObserver =
+ object : ContentObserver(ORDERED_BG_EXECUTOR.handler) {
+ override fun onChange(selfChange: Boolean, uri: Uri?) {
+ if (uri == matchaUri) {
+ isMatchaEnabledInternal =
+ Settings.Secure.getInt(context.contentResolver, MATCHA_SETTING, 0)
+ isMatchaEnabled = isMatchaEnabledInternal != 0
+ }
+ }
+ }
+
+ fun registerObserver() {
+ context.contentResolver.registerContentObserver(matchaUri, false, pillColorObserver)
+ setup()
+ }
+
+ fun unregisterObserver() {
+ context.contentResolver.unregisterContentObserver(pillColorObserver)
+ }
+
+ fun setup() {
+ appTitlePillPaint.color =
+ context.resources.getColor(
+ R.color.material_color_surface_container_lowest,
+ context.theme,
+ )
+ appTitleTextPaint.color =
+ context.resources.getColor(R.color.material_color_on_surface, context.theme)
+ isMatchaEnabledInternal = Settings.Secure.getInt(context.contentResolver, MATCHA_SETTING, 0)
+ isMatchaEnabled = isMatchaEnabledInternal != 0
+ }
+
+ companion object {
+ private var INSTANCE: PillColorProvider? = null
+ private const val MATCHA_SETTING = "matcha_enable"
+
+ // TODO: Replace with a Dagger injection that is a singleton.
+ @JvmStatic
+ fun getInstance(context: Context): PillColorProvider {
+ if (INSTANCE == null) {
+ INSTANCE = PillColorProvider(context)
+ }
+ return INSTANCE!!
+ }
+ }
+}
diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
index ef66ffe..392d9a7 100644
--- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
+++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
@@ -102,6 +102,9 @@
@Override
public void onDraw(Canvas canvas) {
+ if (shouldDrawAppContrastTile()) {
+ drawAppContrastTile(canvas);
+ }
// If text is transparent or shadow alpha is 0, don't draw any shadow
if (skipDoubleShadow()) {
super.onDraw(canvas);
diff --git a/src/com/android/launcher3/views/StickyHeaderLayout.java b/src/com/android/launcher3/views/StickyHeaderLayout.java
index 090251f..4142e1f 100644
--- a/src/com/android/launcher3/views/StickyHeaderLayout.java
+++ b/src/com/android/launcher3/views/StickyHeaderLayout.java
@@ -120,7 +120,19 @@
}
private float getCurrentScroll() {
- return mScrollOffset + (mCurrentEmptySpaceView == null ? 0 : mCurrentEmptySpaceView.getY());
+ float scroll;
+ if (mCurrentRecyclerView.getVisibility() != VISIBLE) {
+ // When no list is displayed, assume no scroll.
+ scroll = 0f;
+ } else if (mCurrentEmptySpaceView != null) {
+ // Otherwise use empty space view as reference to position.
+ scroll = mCurrentEmptySpaceView.getY();
+ } else {
+ // If there is no empty space view, but the list is visible, we are scrolled away
+ // completely, so assume all non-sticky children should also be scrolled away.
+ scroll = -mHeaderHeight;
+ }
+ return mScrollOffset + scroll;
}
@Override
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 2f64ab1..8bebfb2 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -605,9 +605,12 @@
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
mAdapters.get(getCurrentAdapterHolderType()).mWidgetsRecyclerView.setVisibility(
VISIBLE);
- // Visibility of recommended widgets, recycler views and headers are handled in methods
- // below.
- post(this::onRecommendedWidgetsBound);
+ if (mRecommendedWidgetsCount > 0) {
+ // Display recommendations immediately, if present, so that other parts of sticky
+ // header (e.g. personal / work tabs) don't flash in interim.
+ mWidgetRecommendationsContainer.setVisibility(VISIBLE);
+ }
+ // Visibility of recycler views and headers are handled in methods below.
onWidgetsBound();
}
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 3c67538..74a9a5c 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -103,7 +103,7 @@
.equals(mWidgetsContentVisiblePackageUserKey);
@Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
@Nullable private RecyclerView mRecyclerView;
- @Nullable private PackageUserKey mPendingClickHeader;
+ @Nullable private PackageUserKey mHeaderPositionToMaintain;
@Px private int mMaxHorizontalSpan;
private boolean mShowOnlyDefaultList = true;
@@ -215,7 +215,7 @@
// Get the current top of the header with the matching key before adjusting the visible
// entries.
OptionalInt previousPositionForPackageUserKey =
- getPositionForPackageUserKey(mPendingClickHeader);
+ getPositionForPackageUserKey(mHeaderPositionToMaintain);
OptionalInt topForPackageUserKey =
getOffsetForPosition(previousPositionForPackageUserKey);
@@ -247,13 +247,15 @@
mVisibleEntries.addAll(newVisibleEntries);
diffResult.dispatchUpdatesTo(this);
- if (mPendingClickHeader != null) {
+ if (mHeaderPositionToMaintain != null && mRecyclerView != null) {
// Get the position for the clicked header after adjusting the visible entries. The
// position may have changed if another header had previously been expanded.
OptionalInt positionForPackageUserKey =
- getPositionForPackageUserKey(mPendingClickHeader);
- scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey);
- mPendingClickHeader = null;
+ getPositionForPackageUserKey(mHeaderPositionToMaintain);
+ // Post scroll updates to be applied after diff updates.
+ mRecyclerView.post(() -> scrollToPositionAndMaintainOffset(positionForPackageUserKey,
+ topForPackageUserKey));
+ mHeaderPositionToMaintain = null;
}
}
@@ -384,7 +386,7 @@
// Store the header that was clicked so that its position will be maintained the next time
// we update the entries.
- mPendingClickHeader = packageUserKey;
+ mHeaderPositionToMaintain = packageUserKey;
updateVisibleEntries();
@@ -470,6 +472,16 @@
*/
public void useExpandedList() {
mShowOnlyDefaultList = false;
+ if (mWidgetsContentVisiblePackageUserKey != null) {
+ // Maintain selected header for the next update that expands the list.
+ mHeaderPositionToMaintain = mWidgetsContentVisiblePackageUserKey;
+ } else if (mVisibleEntries.size() > 2) {
+ // Maintain last visible header shown above expand button since there was no selected
+ // header.
+ mHeaderPositionToMaintain = PackageUserKey.fromPackageItemInfo(
+ mVisibleEntries.get(mVisibleEntries.size() - 2).mPkgItem);
+ }
+
}
/** Comparator for sorting WidgetListRowEntry based on package title. */
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 68e493d..1ddd453 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -183,7 +183,6 @@
</activity>
<activity-alias android:name="Activity2"
android:label="TestActivity2"
- android:icon="@drawable/test_icon"
android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
@@ -193,7 +192,6 @@
</activity-alias>
<activity-alias android:name="Activity3"
android:label="TestActivity3"
- android:icon="@drawable/test_icon"
android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
@@ -203,7 +201,6 @@
</activity-alias>
<activity-alias android:name="Activity4"
android:label="TestActivity4"
- android:icon="@drawable/test_icon"
android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
@@ -213,7 +210,6 @@
</activity-alias>
<activity-alias android:name="Activity5"
android:label="TestActivity5"
- android:icon="@drawable/test_icon"
android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
@@ -223,7 +219,6 @@
</activity-alias>
<activity-alias android:name="Activity6"
android:label="TestActivity6"
- android:icon="@drawable/test_icon"
android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
@@ -233,7 +228,6 @@
</activity-alias>
<activity-alias android:name="Activity7"
android:label="TestActivity7"
- android:icon="@drawable/test_icon"
android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
@@ -243,7 +237,6 @@
</activity-alias>
<activity-alias android:name="Activity8"
android:label="TestActivity8"
- android:icon="@drawable/test_icon"
android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
@@ -253,7 +246,6 @@
</activity-alias>
<activity-alias android:name="Activity9" android:exported="true"
android:label="TestActivity9"
- android:icon="@drawable/test_icon"
android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -262,7 +254,6 @@
</activity-alias>
<activity-alias android:name="Activity10" android:exported="true"
android:label="TestActivity10"
- android:icon="@drawable/test_icon"
android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -271,7 +262,6 @@
</activity-alias>
<activity-alias android:name="Activity11" android:exported="true"
android:label="TestActivity11"
- android:icon="@drawable/test_icon"
android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -280,7 +270,6 @@
</activity-alias>
<activity-alias android:name="Activity12" android:exported="true"
android:label="TestActivity12"
- android:icon="@drawable/test_icon"
android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -289,7 +278,6 @@
</activity-alias>
<activity-alias android:name="Activity13" android:exported="true"
android:label="TestActivity13"
- android:icon="@drawable/test_icon"
android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -298,7 +286,6 @@
</activity-alias>
<activity-alias android:name="Activity14" android:exported="true"
android:label="TestActivity14"
- android:icon="@drawable/test_icon"
android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -376,7 +363,7 @@
</activity>
<activity android:name="com.android.launcher3.testcomponent.ImeTestActivity"
android:label="ImeTestActivity"
- android:icon="@drawable/test_icon"
+ android:icon="@drawable/test_theme_icon"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
index e8f778f..7cd5da4 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
@@ -18,7 +18,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.LauncherAppState
-import com.android.launcher3.icons.BitmapInfo
import com.android.launcher3.icons.waitForUpdateHandlerToFinish
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.util.Executors
@@ -161,9 +160,6 @@
assertWithMessage("Index $index was not highRes")
.that(items[index].bitmap.isNullOrLowRes)
.isFalse()
- assertWithMessage("Index $index was the default icon")
- .that(isDefaultIcon(items[index].bitmap))
- .isFalse()
}
}
@@ -172,17 +168,9 @@
assertWithMessage("Index $index was not lowRes")
.that(items[index].bitmap.isNullOrLowRes)
.isTrue()
- assertWithMessage("Index $index was the default icon")
- .that(isDefaultIcon(items[index].bitmap))
- .isFalse()
}
}
- private fun isDefaultIcon(bitmap: BitmapInfo) =
- LauncherAppState.getInstance(modelHelper.sandboxContext)
- .iconCache
- .isDefaultIcon(bitmap, modelHelper.sandboxContext.user)
-
/** Recreate DeviceProfiles after changing InvariantDeviceProfile */
private fun recreateSupportedDeviceProfiles() {
LauncherAppState.getIDP(modelHelper.sandboxContext).supportedProfiles =
diff --git a/tests/res/drawable/test_icon.xml b/tests/res/drawable/test_icon.xml
deleted file mode 100644
index 72ebfeb..0000000
--- a/tests/res/drawable/test_icon.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-Copyright (C) 2024 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
- <background android:drawable="@android:color/white"/>
- <foreground>
- <color android:color="#FFFF0000" />
- </foreground>
- <monochrome>
- <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
- </vector>
- </monochrome>
-</adaptive-icon>
diff --git a/tests/src/com/android/launcher3/dragging/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index 59e1f99..e2f9feb9a 100644
--- a/tests/src/com/android/launcher3/dragging/TaplDragTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
@@ -64,7 +64,6 @@
@Test
@PortraitLandscape
@PlatinumTest(focusArea = "launcher")
- @ScreenRecordRule.ScreenRecord // b/353600888
public void testDragToFolder() {
// TODO: add the use case to drag an icon to an existing folder. Currently it either fails
// on tablets or phones due to difference in resolution.
@@ -97,7 +96,6 @@
* icon left.
*/
@Test
- @ScreenRecordRule.ScreenRecord // b/353600888
public void testDragOutOfFolder() {
final HomeAppIcon playStoreIcon = createShortcutIfNotExist(STORE_APP_NAME, 0, 1);
final HomeAppIcon photosIcon = createShortcutInCenterIfNotExist(PHOTOS_APP_NAME);
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index a273648..2b1fddc 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -407,6 +407,15 @@
}
@After
+ public void resetFreezeRecentTaskList() {
+ try {
+ mDevice.executeShellCommand("wm reset-freeze-recent-tasks");
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to reset fozen recent tasks list", e);
+ }
+ }
+
+ @After
public void verifyLauncherState() {
try {
// Limits UI tests affecting tests running after them.
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 08c5552..fac73d3 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1728,6 +1728,27 @@
scrollDownByDistance(container, distance, appsListBottomPadding);
}
+ /** Scrolls up by given distance within the container. */
+ void scrollUpByDistance(UiObject2 container, int distance) {
+ scrollUpByDistance(container, distance, 0);
+ }
+
+ /** Scrolls up by given distance within the container considering the given bottom padding. */
+ void scrollUpByDistance(UiObject2 container, int distance, int bottomPadding) {
+ final Rect containerRect = getVisibleBounds(container);
+ final int bottomGestureMarginInContainer = getBottomGestureMarginInContainer(container);
+ scroll(
+ container,
+ Direction.UP,
+ new Rect(
+ 0,
+ containerRect.height() - bottomGestureMarginInContainer - distance,
+ 0,
+ bottomGestureMarginInContainer + bottomPadding),
+ /* steps= */ 10,
+ /* slowDown= */ true);
+ }
+
void scrollDownByDistance(UiObject2 container, int distance) {
scrollDownByDistance(container, distance, 0);
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 3097d9c..ac2748e 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
import static com.android.launcher3.tapl.LauncherInstrumentation.log;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;
@@ -31,6 +32,7 @@
import com.android.launcher3.testing.shared.TestProtocol;
import java.util.Collection;
+import java.util.List;
/**
* All widgets container.
@@ -128,8 +130,10 @@
final UiObject2 searchBar = findSearchBar();
final int searchBarHeight = searchBar.getVisibleBounds().height();
final UiObject2 fullWidgetsPicker = verifyActiveContainer();
- mLauncher.assertTrue("Widgets container didn't become scrollable",
- fullWidgetsPicker.wait(Until.scrollable(true), WAIT_TIME_MS));
+
+ // Widget picker may not be scrollable if there are few items. Instead of waiting on
+ // picker being scrollable, we wait on widget headers to be available.
+ waitForWidgetListItems(fullWidgetsPicker);
final UiObject2 widgetsContainer =
findTestAppWidgetsTableContainer(testAppWidgetPackage);
@@ -176,6 +180,13 @@
}
}
+ private void waitForWidgetListItems(UiObject2 fullWidgetsPicker) {
+ List<UiObject2> headers = fullWidgetsPicker.wait(Until.findObjects(
+ By.res(mLauncher.getLauncherPackageName(), "widgets_list_header")), WAIT_TIME_MS);
+ mLauncher.assertTrue("Widgets list is not available",
+ headers != null && !headers.isEmpty());
+ }
+
private UiObject2 findSearchBar() {
final BySelector searchBarContainerSelector = By.res(mLauncher.getLauncherPackageName(),
"search_and_recommendations_container");
@@ -199,19 +210,38 @@
"container");
String packageName = mLauncher.getContext().getPackageName();
+ String packageNameToFind =
+ (testAppWidgetPackage == null || testAppWidgetPackage.isEmpty()) ? packageName
+ : testAppWidgetPackage;
+
final BySelector targetAppSelector = By
.clazz("android.widget.TextView")
- .text((testAppWidgetPackage == null || testAppWidgetPackage.isEmpty())
- ? packageName
- : testAppWidgetPackage);
+ .text(packageNameToFind);
+ final BySelector expandListButtonSelector =
+ By.res(mLauncher.getLauncherPackageName(), "widget_list_expand_button");
final BySelector widgetsContainerSelector = By.res(mLauncher.getLauncherPackageName(),
"widgets_table");
boolean hasHeaderExpanded = false;
+ // List was expanded by clicking "Show all" button.
+ boolean hasListExpanded = false;
+
int scrollDistance = 0;
for (int i = 0; i < SCROLL_ATTEMPTS; i++) {
UiObject2 widgetPicker = mLauncher.waitForLauncherObject(widgetPickerSelector);
UiObject2 widgetListView = verifyActiveContainer();
+
+ // Press "Show all" button if it exists. Otherwise, keep scrolling to
+ // find the header or show all button.
+ UiObject2 expandListButton =
+ mLauncher.findObjectInContainer(widgetListView, expandListButtonSelector);
+ if (expandListButton != null) {
+ expandListButton.click();
+ hasListExpanded = true;
+ i = -1;
+ continue;
+ }
+
UiObject2 header = mLauncher.waitForObjectInContainer(widgetListView,
headerSelector);
// If a header is barely visible in the bottom edge of the screen, its height could be
@@ -222,6 +252,17 @@
// Look for a header that has the test app name.
UiObject2 headerTitle = mLauncher.findObjectInContainer(widgetListView,
targetAppSelector);
+
+ final UiObject2 searchBar = findSearchBar();
+ // If header's title is under or above search bar, let's not process the header yet,
+ // scroll a bit more to bring the header into visible area.
+ if (headerTitle != null
+ && headerTitle.getVisibleCenter().y <= searchBar.getVisibleCenter().y) {
+ log("Test app's header is behind the searchbar, scrolling up");
+ mLauncher.scrollUpByDistance(widgetListView, scrollDistance);
+ continue;
+ }
+
if (headerTitle != null) {
// If we find the header and it has not been expanded, let's click it to see the
// widgets list. Note that we wait until the header is out of the gesture region at
@@ -258,11 +299,24 @@
widgetPicker,
widgetsContainerSelector);
- mLauncher.scrollDownByDistance(hasHeaderExpanded && rightPane != null
- ? rightPane
- : widgetListView, scrollDistance);
+ if (hasListExpanded && packageNameToFind.compareToIgnoreCase(
+ getFirstHeaderTitle(widgetListView)) < 0) {
+ mLauncher.scrollUpByDistance(hasHeaderExpanded && rightPane != null
+ ? rightPane
+ : widgetListView, scrollDistance);
+ } else {
+ mLauncher.scrollDownByDistance(hasHeaderExpanded && rightPane != null
+ ? rightPane
+ : widgetListView, scrollDistance);
+ }
}
return null;
}
+
+ @NonNull
+ private String getFirstHeaderTitle(UiObject2 widgetListView) {
+ UiObject2 firstHeader = mLauncher.getObjectsInContainer(widgetListView, "app_title").get(0);
+ return firstHeader != null ? firstHeader.getText() : "";
+ }
}