Merge "Converting LauncherPrefs to dagger" into main
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index c50e82d..c2cabd0 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -229,9 +229,7 @@
(WorkspaceItemInfo) mPredictedItems.get(predictionIndex++);
if (isPredictedIcon(child) && child.isEnabled()) {
PredictedAppIcon icon = (PredictedAppIcon) child;
- boolean animateIconChange = icon.shouldAnimateIconChange(predictedItem);
- icon.applyFromWorkspaceItem(predictedItem, animateIconChange, numViewsAnimated);
- if (animateIconChange) {
+ if (icon.applyFromWorkspaceItemWithAnimation(predictedItem, numViewsAnimated)) {
numViewsAnimated++;
}
icon.finishBinding(mPredictionLongClickListener);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 741853e..6b9f5a9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -64,6 +64,7 @@
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.taskbar.customization.TaskbarAllAppsButtonContainer;
import com.android.launcher3.taskbar.customization.TaskbarDividerContainer;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.LauncherBindableItemsContainer;
import com.android.launcher3.util.Themes;
@@ -595,10 +596,12 @@
// Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
if (hotseatView instanceof BubbleTextView btv
&& hotseatItemInfo instanceof WorkspaceItemInfo workspaceInfo) {
- boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo);
- btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated);
- if (animate) {
- numViewsAnimated++;
+ if (btv instanceof PredictedAppIcon pai) {
+ if (pai.applyFromWorkspaceItemWithAnimation(workspaceInfo, numViewsAnimated)) {
+ numViewsAnimated++;
+ }
+ } else {
+ btv.applyFromWorkspaceItem(workspaceInfo);
}
}
setClickAndLongClickListenersForIcon(hotseatView);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index fd4cf0e..0abd88c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -299,7 +299,7 @@
private final PointF mTouchDownLocation = new PointF();
private final PointF mViewInitialPosition = new PointF();
private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
- private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout() / 2;
+ private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout();
private State mState = State.IDLE;
private int mTouchSlop = -1;
private BubbleDragAnimator mAnimator;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
deleted file mode 100644
index 721c831..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2019 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.uioverrides;
-
-import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
-import static com.android.app.animation.Interpolators.AGGRESSIVE_EASE_IN_OUT;
-import static com.android.app.animation.Interpolators.FINAL_FRAME;
-import static com.android.app.animation.Interpolators.INSTANT;
-import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
-import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
-import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
-import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
-import static com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS;
-import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
-import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
-import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
-import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA;
-
-import android.util.FloatProperty;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.statemanager.StateManager.StateHandler;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.quickstep.views.RecentsView;
-
-/**
- * State handler for recents view. Manages UI changes and animations for recents view based off the
- * current {@link LauncherState}.
- *
- * @param <T> the recents view
- */
-public abstract class BaseRecentsViewStateController<T extends RecentsView>
- implements StateHandler<LauncherState> {
- protected final T mRecentsView;
- protected final QuickstepLauncher mLauncher;
-
- public BaseRecentsViewStateController(@NonNull QuickstepLauncher launcher) {
- mLauncher = launcher;
- mRecentsView = launcher.getOverviewPanel();
- }
-
- @Override
- public void setState(@NonNull LauncherState state) {
- float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
- RECENTS_SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
- ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, scaleAndOffset[1]);
- TASK_SECONDARY_TRANSLATION.set(mRecentsView, 0f);
-
- getContentAlphaProperty().set(mRecentsView, state.isRecentsViewVisible ? 1f : 0);
- getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness());
- RECENTS_GRID_PROGRESS.set(mRecentsView,
- state.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f);
- TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, state.showTaskThumbnailSplash() ? 1f : 0f);
- if (enableLargeDesktopWindowingTile()) {
- DESKTOP_CAROUSEL_DETACH_PROGRESS.set(mRecentsView,
- state.detachDesktopCarousel() ? 1f : 0f);
- }
- }
-
- @Override
- public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
- PendingAnimation builder) {
- if (config.hasAnimationFlag(SKIP_OVERVIEW)) {
- return;
- }
- setStateWithAnimationInternal(toState, config, builder);
- builder.addEndListener(success -> {
- if (!success && !toState.isRecentsViewVisible) {
- mRecentsView.reset();
- }
- });
- }
-
- /**
- * Core logic for animating the recents view UI.
- *
- * @param toState state to animate to
- * @param config current animation config
- * @param setter animator set builder
- */
- void setStateWithAnimationInternal(@NonNull final LauncherState toState,
- @NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
- float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
- setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
- config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
- setter.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1],
- config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
- setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f,
- config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
-
- boolean exitingOverview =
- !FeatureFlags.enableSplitContextually() && !toState.isRecentsViewVisible;
- if (mRecentsView.isSplitSelectionActive() && exitingOverview) {
- setter.add(mRecentsView.getSplitSelectController().getSplitAnimationController()
- .createPlaceholderDismissAnim(mLauncher, LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
- setter.getDuration()));
- setter.setViewAlpha(
- mRecentsView.getSplitInstructionsView(),
- 0,
- config.getInterpolator(
- ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE,
- LINEAR
- )
- );
- }
-
- setter.setFloat(mRecentsView, getContentAlphaProperty(),
- toState.isRecentsViewVisible ? 1 : 0,
- config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
-
- setter.setFloat(
- mRecentsView, getTaskModalnessProperty(),
- toState.getOverviewModalness(),
- config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
-
- LauncherState fromState = mLauncher.getStateManager().getState();
- setter.setFloat(mRecentsView, TASK_THUMBNAIL_SPLASH_ALPHA,
- toState.showTaskThumbnailSplash() ? 1f : 0f,
- getOverviewInterpolator(fromState, toState));
-
- setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS,
- toState.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f,
- getOverviewInterpolator(fromState, toState));
-
- if (enableLargeDesktopWindowingTile()) {
- setter.setFloat(mRecentsView, DESKTOP_CAROUSEL_DETACH_PROGRESS,
- toState.detachDesktopCarousel() ? 1f : 0f,
- getOverviewInterpolator(fromState, toState));
- }
- }
-
- private Interpolator getOverviewInterpolator(LauncherState fromState, LauncherState toState) {
- return fromState == QUICK_SWITCH_FROM_HOME
- ? ACCELERATE_DECELERATE
- : toState.isRecentsViewVisible ? INSTANT : FINAL_FRAME;
- }
-
- abstract FloatProperty getTaskModalnessProperty();
-
- /**
- * Get property for content alpha for the recents view.
- *
- * @return the float property for the view's content alpha
- */
- abstract FloatProperty getContentAlphaProperty();
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 535ae1c..caac35e 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -16,7 +16,6 @@
package com.android.launcher3.uioverrides;
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
-import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
import android.animation.Animator;
@@ -26,8 +25,6 @@
import android.animation.Keyframe;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
-import android.animation.ValueAnimator;
-import android.annotation.Nullable;
import android.content.Context;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
@@ -37,7 +34,6 @@
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
-import android.os.Process;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Property;
@@ -48,12 +44,12 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.DelegatedCellDrawing;
-import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.icons.LauncherIcons;
@@ -64,10 +60,6 @@
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.DoubleShadowBubbleTextView;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
/**
* A BubbleTextView with a ring around it's drawable
*/
@@ -105,12 +97,12 @@
private final BlurMaskFilter mShadowFilter;
private boolean mIsPinned = false;
- private int mPlateColor;
+ private final AnimColorHolder mPlateColor = new AnimColorHolder();
boolean mDrawForDrag = false;
- // Used for the "slot-machine" education animation.
- private List<Drawable> mSlotMachineIcons;
- private Animator mSlotMachineAnim;
+ // Used for the "slot-machine" animation when prediction changes.
+ private final Rect mSlotIconBound = new Rect(0, 0, getIconSize(), getIconSize());
+ private Drawable mSlotMachineIcon;
private float mSlotMachineIconTranslationY;
// Used to animate the "ring" around predicted icons
@@ -153,34 +145,26 @@
@Override
public void onDraw(Canvas canvas) {
int count = canvas.save();
- boolean isSlotMachineAnimRunning = mSlotMachineAnim != null;
+ boolean isSlotMachineAnimRunning = mSlotMachineIcon != null;
if (!mIsPinned) {
drawEffect(canvas);
if (isSlotMachineAnimRunning) {
// Clip to to outside of the ring during the slot machine animation.
canvas.clipPath(mRingPath);
}
- canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
- canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
+ canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO,
+ getWidth() * .5f, getHeight() * .5f);
+ if (isSlotMachineAnimRunning) {
+ canvas.translate(0, mSlotMachineIconTranslationY);
+ mSlotMachineIcon.setBounds(mSlotIconBound);
+ mSlotMachineIcon.draw(canvas);
+ canvas.translate(0, getSlotMachineIconPlusSpacingSize());
+ }
}
- if (isSlotMachineAnimRunning) {
- drawSlotMachineIcons(canvas);
- } else {
- super.onDraw(canvas);
- }
+ super.onDraw(canvas);
canvas.restoreToCount(count);
}
- private void drawSlotMachineIcons(Canvas canvas) {
- canvas.translate((getWidth() - getIconSize()) / 2f,
- (getHeight() - getIconSize()) / 2f + mSlotMachineIconTranslationY);
- for (Drawable icon : mSlotMachineIcons) {
- icon.setBounds(0, 0, getIconSize(), getIconSize());
- icon.draw(canvas);
- canvas.translate(0, getSlotMachineIconPlusSpacingSize());
- }
- }
-
private float getSlotMachineIconPlusSpacingSize() {
return getIconSize() + getOutlineOffsetY();
}
@@ -196,104 +180,88 @@
mIsDrawingDot = false;
}
- @Override
- public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
- // Create the slot machine animation first, since it uses the current icon to start.
- Animator slotMachineAnim = animate
- ? createSlotMachineAnim(Collections.singletonList(info.bitmap), false)
- : null;
- super.applyFromWorkspaceItem(info, animate, staggerIndex);
- int oldPlateColor = mPlateColor;
+ /**
+ * Returns whether the newInfo differs from the current getTag().
+ */
+ private boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) {
+ boolean changedIcons = getTag() instanceof WorkspaceItemInfo oldInfo
+ && oldInfo.getTargetComponent() != null
+ && newInfo.getTargetComponent() != null
+ && !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent());
+ return changedIcons && isShown();
+ }
- int newPlateColor;
+ @Override
+ public void applyIconAndLabel(ItemInfoWithIcon info) {
+ super.applyIconAndLabel(info);
if (getIcon().isThemed()) {
- newPlateColor = getResources().getColor(android.R.color.system_accent1_300);
+ mPlateColor.endColor = getResources().getColor(android.R.color.system_accent1_300);
} else {
float[] hctPlateColor = new float[3];
ColorUtils.colorToM3HCT(mDotParams.appColor, hctPlateColor);
- newPlateColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85);
+ mPlateColor.endColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85);
}
+ mPlateColor.onUpdate();
+ }
+
+ /**
+ * Tries to apply the icon with animation and returns true if the icon was indeed animated
+ */
+ public boolean applyFromWorkspaceItemWithAnimation(WorkspaceItemInfo info, int staggerIndex) {
+ boolean animate = shouldAnimateIconChange(info);
+ Drawable oldIcon = getIcon();
+ int oldPlateColor = mPlateColor.currentColor;
+ applyFromWorkspaceItem(info, null);
+
+ setContentDescription(
+ mIsPinned ? info.contentDescription :
+ getContext().getString(R.string.hotseat_prediction_content_description,
+ info.contentDescription));
if (!animate) {
- mPlateColor = newPlateColor;
- }
- if (mIsPinned) {
- setContentDescription(info.contentDescription);
+ mPlateColor.startColor = mPlateColor.endColor;
+ mPlateColor.progress.value = 1;
+ mPlateColor.onUpdate();
} else {
- setContentDescription(
- getContext().getString(R.string.hotseat_prediction_content_description,
- info.contentDescription));
- }
+ mPlateColor.startColor = oldPlateColor;
+ mPlateColor.progress.value = 0;
+ mPlateColor.onUpdate();
- if (animate) {
- ValueAnimator plateColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(),
- oldPlateColor, newPlateColor);
- plateColorAnim.addUpdateListener(valueAnimator -> {
- mPlateColor = (int) valueAnimator.getAnimatedValue();
- invalidate();
- });
AnimatorSet changeIconAnim = new AnimatorSet();
- if (slotMachineAnim != null) {
+
+ ObjectAnimator plateColorAnim =
+ ObjectAnimator.ofFloat(mPlateColor.progress, AnimatedFloat.VALUE, 0, 1);
+ plateColorAnim.setAutoCancel(true);
+ changeIconAnim.play(plateColorAnim);
+
+ if (!mIsPinned && oldIcon != null) {
+ // Play the slot machine icon
+ mSlotMachineIcon = oldIcon;
+
+ float finalTrans = -getSlotMachineIconPlusSpacingSize();
+ Keyframe[] keyframes = new Keyframe[] {
+ Keyframe.ofFloat(0f, 0f),
+ Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot
+ Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position
+ };
+ keyframes[1].setInterpolator(ACCELERATE_DECELERATE);
+ keyframes[2].setInterpolator(ACCELERATE_DECELERATE);
+
+ ObjectAnimator slotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this,
+ PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes));
+ slotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> {
+ mSlotMachineIcon = null;
+ mSlotMachineIconTranslationY = 0;
+ invalidate();
+ }));
+ slotMachineAnim.setAutoCancel(true);
changeIconAnim.play(slotMachineAnim);
}
- changeIconAnim.play(plateColorAnim);
+
changeIconAnim.setStartDelay(staggerIndex * ICON_CHANGE_ANIM_STAGGER);
changeIconAnim.setDuration(ICON_CHANGE_ANIM_DURATION).start();
}
- }
-
- /**
- * Returns an Animator that translates the given icons in a "slot-machine" fashion, beginning
- * and ending with the original icon.
- */
- public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate) {
- return createSlotMachineAnim(iconsToAnimate, true);
- }
-
- /**
- * Returns an Animator that translates the given icons in a "slot-machine" fashion, beginning
- * with the original icon, then cycling through the given icons, optionally ending back with
- * the original icon.
- * @param endWithOriginalIcon Whether we should land back on the icon we started with, rather
- * than the last item in iconsToAnimate.
- */
- public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate,
- boolean endWithOriginalIcon) {
- if (mIsPinned || iconsToAnimate == null || iconsToAnimate.isEmpty()) {
- return null;
- }
- if (mSlotMachineAnim != null) {
- mSlotMachineAnim.end();
- }
-
- // Bookend the other animating icons with the original icon on both ends.
- mSlotMachineIcons = new ArrayList<>(iconsToAnimate.size() + 2);
- mSlotMachineIcons.add(getIcon());
- iconsToAnimate.stream()
- .map(iconInfo -> iconInfo.newIcon(mContext, FLAG_THEMED))
- .forEach(mSlotMachineIcons::add);
- if (endWithOriginalIcon) {
- mSlotMachineIcons.add(getIcon());
- }
-
- float finalTrans = -getSlotMachineIconPlusSpacingSize() * (mSlotMachineIcons.size() - 1);
- Keyframe[] keyframes = new Keyframe[] {
- Keyframe.ofFloat(0f, 0f),
- Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot
- Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position
- };
- keyframes[1].setInterpolator(ACCELERATE_DECELERATE);
- keyframes[2].setInterpolator(ACCELERATE_DECELERATE);
-
- mSlotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this,
- PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes));
- mSlotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> {
- mSlotMachineIcons = null;
- mSlotMachineAnim = null;
- mSlotMachineIconTranslationY = 0;
- invalidate();
- }));
- return mSlotMachineAnim;
+ return animate;
}
/**
@@ -345,6 +313,7 @@
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
+ mSlotIconBound.offsetTo((w - getIconSize()) / 2, (h - getIconSize()) / 2);
updateRingPath();
}
@@ -355,18 +324,12 @@
}
private void updateRingPath() {
- boolean isBadged = false;
- if (getTag() instanceof WorkspaceItemInfo) {
- WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
- isBadged = !Process.myUserHandle().equals(info.user)
- || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
- }
-
mRingPath.reset();
mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY());
-
mRingPath.addPath(mShapePath, mTmpMatrix);
- if (isBadged) {
+
+ FastBitmapDrawable icon = getIcon();
+ if (icon != null && icon.getBadge() != null) {
float outlineSize = mNormalizedIconSize * RING_EFFECT_RATIO;
float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
@@ -422,7 +385,7 @@
canvas.scale(mRingScale, mRingScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
}
canvas.drawPath(mRingPath, mIconRingPaint);
- mIconRingPaint.setColor(mPlateColor);
+ mIconRingPaint.setColor(mPlateColor.currentColor);
mIconRingPaint.setMaskFilter(null);
canvas.drawPath(mRingPath, mIconRingPaint);
canvas.restoreToCount(count);
@@ -474,6 +437,21 @@
return icon;
}
+ private class AnimColorHolder {
+
+ public final AnimatedFloat progress = new AnimatedFloat(this::onUpdate, 1);
+ public final ArgbEvaluator evaluator = ArgbEvaluator.getInstance();
+ public Integer startColor = 0;
+ public Integer endColor = 0;
+
+ public int currentColor = 0;
+
+ private void onUpdate() {
+ currentColor = (Integer) evaluator.evaluate(progress.value, startColor, endColor);
+ invalidate();
+ }
+ }
+
/**
* Draws Predicted Icon outline on cell layout
*/
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
deleted file mode 100644
index 111069f..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2017 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.uioverrides;
-
-import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
-import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
-import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
-import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
-import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
-import static com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLATION;
-import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION;
-import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL;
-import static com.android.wm.shell.Flags.enableSplitContextual;
-
-import android.animation.AnimatorSet;
-import android.annotation.TargetApi;
-import android.os.Build;
-import android.util.FloatProperty;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.anim.AnimatorListeners;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.AnimUtils;
-import com.android.quickstep.util.SplitAnimationTimings;
-import com.android.quickstep.views.ClearAllButton;
-import com.android.quickstep.views.LauncherRecentsView;
-import com.android.quickstep.views.RecentsView;
-
-/**
- * State handler for handling UI changes for {@link LauncherRecentsView}. In addition to managing
- * the basic view properties, this class also manages changes in the task visuals.
- */
-@TargetApi(Build.VERSION_CODES.O)
-public final class RecentsViewStateController extends
- BaseRecentsViewStateController<LauncherRecentsView> {
-
- public RecentsViewStateController(QuickstepLauncher launcher) {
- super(launcher);
- }
-
- @Override
- public void setState(@NonNull LauncherState state) {
- super.setState(state);
- if (state.isRecentsViewVisible) {
- mRecentsView.updateEmptyMessage();
- } else {
- mRecentsView.resetTaskVisuals();
- }
- setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig(), state);
- mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
- // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
- // DepthController to prevent optimizations which might occlude the layers behind
- mLauncher.getDepthController().setHasContentBehindLauncher(state.isRecentsViewVisible);
-
- PendingAnimation builder =
- new PendingAnimation(state.getTransitionDuration(mLauncher, true));
-
- handleSplitSelectionState(state, builder, /* animate */false);
- }
-
- @Override
- void setStateWithAnimationInternal(@NonNull LauncherState toState,
- @NonNull StateAnimationConfig config, @NonNull PendingAnimation builder) {
- super.setStateWithAnimationInternal(toState, config, builder);
-
- if (toState.isRecentsViewVisible) {
- // While animating into recents, update the visible task data as needed
- builder.addOnFrameCallback(() -> mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL));
- mRecentsView.updateEmptyMessage();
- // TODO(b/246283207): Remove logging once root cause of flake detected.
- if (Utilities.isRunningInTestHarness()) {
- Log.d("b/246283207", "RecentsView#setStateWithAnimationInternal getCurrentPage(): "
- + mRecentsView.getCurrentPage()
- + ", getScrollForPage(getCurrentPage())): "
- + mRecentsView.getScrollForPage(mRecentsView.getCurrentPage()));
- }
- } else {
- builder.addListener(
- AnimatorListeners.forSuccessCallback(mRecentsView::resetTaskVisuals));
- }
- // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
- // DepthController to prevent optimizations which might occlude the layers behind
- builder.addListener(AnimatorListeners.forSuccessCallback(() ->
- mLauncher.getDepthController().setHasContentBehindLauncher(
- toState.isRecentsViewVisible)));
-
- handleSplitSelectionState(toState, builder, /* animate */true);
-
- setAlphas(builder, config, toState);
- builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
- toState.getOverviewFullscreenProgress(), LINEAR);
- }
-
- /**
- * Create or dismiss split screen select animations.
- * @param builder if null then this will run the split select animations right away, otherwise
- * will add animations to builder.
- */
- private void handleSplitSelectionState(@NonNull LauncherState toState,
- @NonNull PendingAnimation builder, boolean animate) {
- boolean goingToOverviewFromWorkspaceContextual = enableSplitContextual() &&
- toState == OVERVIEW && mLauncher.isSplitSelectionActive();
- if (toState != OVERVIEW_SPLIT_SELECT && !goingToOverviewFromWorkspaceContextual) {
- // Not going to split
- return;
- }
-
- // Create transition animations to split select
- RecentsPagedOrientationHandler orientationHandler =
- ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler();
- Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
- orientationHandler.getSplitSelectTaskOffset(
- TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
- mLauncher.getDeviceProfile());
-
- SplitAnimationTimings timings =
- AnimUtils.getDeviceOverviewToSplitTimings(mLauncher.getDeviceProfile().isTablet);
- if (!goingToOverviewFromWorkspaceContextual) {
- // This animation is already done for the contextual case, don't redo it
- mRecentsView.createSplitSelectInitAnimation(builder,
- toState.getTransitionDuration(mLauncher, true /* isToState */));
- }
- // Shift tasks vertically downward to get out of placeholder view
- builder.setFloat(mRecentsView, taskViewsFloat.first,
- toState.getSplitSelectTranslation(mLauncher),
- timings.getGridSlidePrimaryInterpolator());
- // Zero out horizontal translation
- builder.setFloat(mRecentsView, taskViewsFloat.second,
- 0,
- timings.getGridSlideSecondaryInterpolator());
-
- mRecentsView.handleDesktopTaskInSplitSelectState(builder,
- timings.getDesktopTaskFadeInterpolator());
-
- if (!animate) {
- AnimatorSet as = builder.buildAnim();
- as.start();
- as.end();
- }
- }
-
- private void setAlphas(PropertySetter propertySetter, StateAnimationConfig config,
- LauncherState state) {
- float clearAllButtonAlpha = state.areElementsVisible(mLauncher, CLEAR_ALL_BUTTON) ? 1 : 0;
- propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
- clearAllButtonAlpha, LINEAR);
- float overviewButtonAlpha = state.areElementsVisible(mLauncher, OVERVIEW_ACTIONS) ? 1 : 0;
- propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
- AnimatedFloat.VALUE, overviewButtonAlpha, config.getInterpolator(
- ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
- }
-
- @Override
- FloatProperty<RecentsView> getTaskModalnessProperty() {
- return TASK_MODALNESS;
- }
-
- @Override
- FloatProperty<RecentsView> getContentAlphaProperty() {
- return CONTENT_ALPHA;
- }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
new file mode 100644
index 0000000..f196548
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides
+
+import com.android.app.animation.Interpolators.ACCELERATE_DECELERATE
+import com.android.app.animation.Interpolators.AGGRESSIVE_EASE_IN_OUT
+import com.android.app.animation.Interpolators.FINAL_FRAME
+import com.android.app.animation.Interpolators.INSTANT
+import com.android.app.animation.Interpolators.LINEAR
+import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
+import com.android.launcher3.LauncherState
+import com.android.launcher3.anim.AnimatedFloat
+import com.android.launcher3.anim.AnimatorListeners.forSuccessCallback
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.anim.PropertySetter
+import com.android.launcher3.config.FeatureFlags.enableSplitContextually
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent
+import com.android.launcher3.statemanager.StateManager.StateHandler
+import com.android.launcher3.states.StateAnimationConfig
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y
+import com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW
+import com.android.quickstep.util.AnimUtils
+import com.android.quickstep.views.ClearAllButton
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET
+import com.android.quickstep.views.RecentsView.CONTENT_ALPHA
+import com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS
+import com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS
+import com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS
+import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY
+import com.android.quickstep.views.RecentsView.TASK_MODALNESS
+import com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLATION
+import com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION
+import com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION
+import com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA
+import com.android.quickstep.views.TaskView.Companion.FLAG_UPDATE_ALL
+import com.android.wm.shell.Flags.enableSplitContextual
+
+/**
+ * State handler for handling UI changes for [com.android.quickstep.views.LauncherRecentsView]. In
+ * addition to managing the basic view properties, this class also manages changes in the task
+ * visuals.
+ */
+class RecentsViewStateController(private val launcher: QuickstepLauncher) :
+ StateHandler<LauncherState> {
+ private val recentsView: RecentsView<*, *> = launcher.getOverviewPanel()
+
+ override fun setState(state: LauncherState) {
+ val scaleAndOffset = state.getOverviewScaleAndOffset(launcher)
+ RECENTS_SCALE_PROPERTY.set(recentsView, scaleAndOffset[0])
+ ADJACENT_PAGE_HORIZONTAL_OFFSET.set(recentsView, scaleAndOffset[1])
+ TASK_SECONDARY_TRANSLATION.set(recentsView, 0f)
+
+ CONTENT_ALPHA.set(recentsView, if (state.isRecentsViewVisible) 1f else 0f)
+ TASK_MODALNESS.set(recentsView, state.overviewModalness)
+ RECENTS_GRID_PROGRESS.set(
+ recentsView,
+ if (state.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f,
+ )
+ TASK_THUMBNAIL_SPLASH_ALPHA.set(
+ recentsView,
+ if (state.showTaskThumbnailSplash()) 1f else 0f,
+ )
+ if (enableLargeDesktopWindowingTile()) {
+ DESKTOP_CAROUSEL_DETACH_PROGRESS.set(
+ recentsView,
+ if (state.detachDesktopCarousel()) 1f else 0f,
+ )
+ }
+
+ if (state.isRecentsViewVisible) {
+ recentsView.updateEmptyMessage()
+ } else {
+ recentsView.resetTaskVisuals()
+ }
+ setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, StateAnimationConfig(), state)
+ recentsView.setFullscreenProgress(state.overviewFullscreenProgress)
+ // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
+ // DepthController to prevent optimizations which might occlude the layers behind
+ launcher.depthController.setHasContentBehindLauncher(state.isRecentsViewVisible)
+
+ val builder = PendingAnimation(state.getTransitionDuration(launcher, true).toLong())
+ handleSplitSelectionState(state, builder, animate = false)
+ }
+
+ override fun setStateWithAnimation(
+ toState: LauncherState,
+ config: StateAnimationConfig,
+ builder: PendingAnimation,
+ ) {
+ if (config.hasAnimationFlag(SKIP_OVERVIEW)) return
+
+ val scaleAndOffset = toState.getOverviewScaleAndOffset(launcher)
+ builder.setFloat(
+ recentsView,
+ RECENTS_SCALE_PROPERTY,
+ scaleAndOffset[0],
+ config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR),
+ )
+ builder.setFloat(
+ recentsView,
+ ADJACENT_PAGE_HORIZONTAL_OFFSET,
+ scaleAndOffset[1],
+ config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR),
+ )
+ builder.setFloat(
+ recentsView,
+ TASK_SECONDARY_TRANSLATION,
+ 0f,
+ config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR),
+ )
+
+ val exitingOverview = !enableSplitContextually() && !toState.isRecentsViewVisible
+ if (recentsView.isSplitSelectionActive && exitingOverview) {
+ builder.add(
+ recentsView.splitSelectController.splitAnimationController
+ .createPlaceholderDismissAnim(
+ launcher,
+ LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
+ builder.duration,
+ )
+ )
+ builder.setViewAlpha(
+ recentsView.splitInstructionsView,
+ 0f,
+ config.getInterpolator(ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE, LINEAR),
+ )
+ }
+
+ builder.setFloat(
+ recentsView,
+ CONTENT_ALPHA,
+ if (toState.isRecentsViewVisible) 1f else 0f,
+ config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT),
+ )
+
+ builder.setFloat(
+ recentsView,
+ TASK_MODALNESS,
+ toState.overviewModalness,
+ config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR),
+ )
+
+ val fromState = launcher.stateManager.state
+ builder.setFloat(
+ recentsView,
+ TASK_THUMBNAIL_SPLASH_ALPHA,
+ if (toState.showTaskThumbnailSplash()) 1f else 0f,
+ getOverviewInterpolator(fromState, toState),
+ )
+
+ builder.setFloat(
+ recentsView,
+ RECENTS_GRID_PROGRESS,
+ if (toState.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f,
+ getOverviewInterpolator(fromState, toState),
+ )
+
+ if (enableLargeDesktopWindowingTile()) {
+ builder.setFloat(
+ recentsView,
+ DESKTOP_CAROUSEL_DETACH_PROGRESS,
+ if (toState.detachDesktopCarousel()) 1f else 0f,
+ getOverviewInterpolator(fromState, toState),
+ )
+ }
+
+ if (toState.isRecentsViewVisible) {
+ // While animating into recents, update the visible task data as needed
+ builder.addOnFrameCallback { recentsView.loadVisibleTaskData(FLAG_UPDATE_ALL) }
+ recentsView.updateEmptyMessage()
+ } else {
+ builder.addListener(forSuccessCallback { recentsView.resetTaskVisuals() })
+ }
+ // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
+ // DepthController to prevent optimizations which might occlude the layers behind
+ builder.addListener(
+ forSuccessCallback {
+ launcher.depthController.setHasContentBehindLauncher(toState.isRecentsViewVisible)
+ }
+ )
+
+ handleSplitSelectionState(toState, builder, animate = true)
+
+ setAlphas(builder, config, toState)
+ builder.setFloat(
+ recentsView,
+ FULLSCREEN_PROGRESS,
+ toState.overviewFullscreenProgress,
+ LINEAR,
+ )
+
+ builder.addEndListener { success: Boolean ->
+ if (!success && !toState.isRecentsViewVisible) {
+ recentsView.reset()
+ }
+ }
+ }
+
+ /**
+ * Create or dismiss split screen select animations.
+ *
+ * @param builder if null then this will run the split select animations right away, otherwise
+ * will add animations to builder.
+ */
+ private fun handleSplitSelectionState(
+ toState: LauncherState,
+ builder: PendingAnimation,
+ animate: Boolean,
+ ) {
+ val goingToOverviewFromWorkspaceContextual =
+ enableSplitContextual() &&
+ toState == LauncherState.OVERVIEW &&
+ launcher.isSplitSelectionActive
+ if (
+ toState != LauncherState.OVERVIEW_SPLIT_SELECT &&
+ !goingToOverviewFromWorkspaceContextual
+ ) {
+ // Not going to split
+ return
+ }
+
+ // Create transition animations to split select
+ val orientationHandler = recentsView.pagedOrientationHandler
+ val taskViewsFloat =
+ orientationHandler.getSplitSelectTaskOffset(
+ TASK_PRIMARY_SPLIT_TRANSLATION,
+ TASK_SECONDARY_SPLIT_TRANSLATION,
+ launcher.deviceProfile,
+ )
+
+ val timings = AnimUtils.getDeviceOverviewToSplitTimings(launcher.deviceProfile.isTablet)
+ if (!goingToOverviewFromWorkspaceContextual) {
+ // This animation is already done for the contextual case, don't redo it
+ recentsView.createSplitSelectInitAnimation(
+ builder,
+ toState.getTransitionDuration(launcher, true),
+ )
+ }
+ // Shift tasks vertically downward to get out of placeholder view
+ builder.setFloat(
+ recentsView,
+ taskViewsFloat.first,
+ toState.getSplitSelectTranslation(launcher),
+ timings.gridSlidePrimaryInterpolator,
+ )
+ // Zero out horizontal translation
+ builder.setFloat(
+ recentsView,
+ taskViewsFloat.second,
+ 0f,
+ timings.gridSlideSecondaryInterpolator,
+ )
+
+ recentsView.handleDesktopTaskInSplitSelectState(
+ builder,
+ timings.desktopTaskFadeInterpolator,
+ )
+
+ if (!animate) {
+ builder.buildAnim().apply {
+ start()
+ end()
+ }
+ }
+ }
+
+ private fun setAlphas(
+ propertySetter: PropertySetter,
+ config: StateAnimationConfig,
+ state: LauncherState,
+ ) {
+ val clearAllButtonAlpha =
+ if (state.areElementsVisible(launcher, LauncherState.CLEAR_ALL_BUTTON)) 1f else 0f
+ propertySetter.setFloat(
+ recentsView.clearAllButton,
+ ClearAllButton.VISIBILITY_ALPHA,
+ clearAllButtonAlpha,
+ LINEAR,
+ )
+ val overviewButtonAlpha =
+ if (state.areElementsVisible(launcher, LauncherState.OVERVIEW_ACTIONS)) 1f else 0f
+ propertySetter.setFloat(
+ launcher.actionsView.visibilityAlpha,
+ AnimatedFloat.VALUE,
+ overviewButtonAlpha,
+ config.getInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, LINEAR),
+ )
+ }
+
+ private fun getOverviewInterpolator(fromState: LauncherState, toState: LauncherState) =
+ when {
+ fromState == LauncherState.QUICK_SWITCH_FROM_HOME -> ACCELERATE_DECELERATE
+ toState.isRecentsViewVisible -> INSTANT
+ else -> FINAL_FRAME
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/HighResLoadingState.kt b/quickstep/src/com/android/quickstep/HighResLoadingState.kt
new file mode 100644
index 0000000..8a21c4f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/HighResLoadingState.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep
+
+import android.content.res.Resources
+import com.android.quickstep.recents.data.HighResLoadingStateNotifier
+
+/** Determines when high res or low res thumbnails should be loaded. */
+class HighResLoadingState : HighResLoadingStateNotifier {
+ // If the device does not support low-res thumbnails, only attempt to load high-res thumbnails
+ private val forceHighResThumbnails = !supportsLowResThumbnails()
+ var visible: Boolean = false
+ set(value) {
+ field = value
+ updateState()
+ }
+
+ var flingingFast = false
+ set(value) {
+ field = value
+ updateState()
+ }
+
+ var isEnabled: Boolean = false
+ private set
+
+ private val callbacks = ArrayList<HighResLoadingStateChangedCallback>()
+
+ interface HighResLoadingStateChangedCallback {
+ fun onHighResLoadingStateChanged(enabled: Boolean)
+ }
+
+ override fun addCallback(callback: HighResLoadingStateChangedCallback) {
+ callbacks.add(callback)
+ }
+
+ override fun removeCallback(callback: HighResLoadingStateChangedCallback) {
+ callbacks.remove(callback)
+ }
+
+ private fun updateState() {
+ val prevState = isEnabled
+ isEnabled = forceHighResThumbnails || (visible && !flingingFast)
+ if (prevState != isEnabled) {
+ for (callback in callbacks.asReversed()) {
+ callback.onHighResLoadingStateChanged(isEnabled)
+ }
+ }
+ }
+
+ /**
+ * Returns Whether device supports low-res thumbnails. Low-res files are an optimization for
+ * faster load times of snapshots. Devices can optionally disable low-res files so that they
+ * only store snapshots at high-res scale. The actual scale can be configured in frameworks/base
+ * config overlay.
+ */
+ private fun supportsLowResThumbnails(): Boolean {
+ val res = Resources.getSystem()
+ val resId = res.getIdentifier("config_lowResTaskSnapshotScale", "dimen", "android")
+ if (resId != 0) {
+ return 0 < res.getFloat(resId)
+ }
+ return true
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
deleted file mode 100644
index c4221a1..0000000
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ /dev/null
@@ -1,308 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.Flags.enableOverviewIconMenu;
-import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.ActivityManager.TaskDescription;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.SparseArray;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.icons.BaseIconFactory;
-import com.android.launcher3.icons.BaseIconFactory.IconOptions;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconProvider;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.CancellableTask;
-import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
-import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.FlagOp;
-import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.task.thumbnail.data.TaskIconDataSource;
-import com.android.quickstep.util.TaskKeyLruCache;
-import com.android.quickstep.util.TaskVisualsChangeListener;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.system.PackageManagerWrapper;
-
-import java.util.concurrent.Executor;
-
-/**
- * Manages the caching of task icons and related data.
- */
-public class TaskIconCache implements TaskIconDataSource, DisplayInfoChangeListener {
-
- private final Executor mBgExecutor;
-
- private final Context mContext;
- private final TaskKeyLruCache<TaskCacheEntry> mIconCache;
- private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
- private BitmapInfo mDefaultIconBase = null;
-
- private final IconProvider mIconProvider;
-
- private BaseIconFactory mIconFactory;
-
- @Nullable
- public TaskVisualsChangeListener mTaskVisualsChangeListener = null;
-
- public TaskIconCache(Context context, Executor bgExecutor, IconProvider iconProvider) {
- mContext = context;
- mBgExecutor = bgExecutor;
- mIconProvider = iconProvider;
-
- Resources res = context.getResources();
- int cacheSize = res.getInteger(R.integer.recentsIconCacheSize);
-
- mIconCache = new TaskKeyLruCache<>(cacheSize);
-
- DisplayController.INSTANCE.get(mContext).addChangeListener(this);
- }
-
- @Override
- public void onDisplayInfoChanged(Context context, Info info, int flags) {
- if ((flags & CHANGE_DENSITY) != 0) {
- clearCache();
- }
- }
-
- /**
- * Asynchronously fetches the icon and other task data.
- *
- * @param task The task to fetch the data for
- * @param callback The callback to receive the task after its data has been populated.
- * @return A cancelable handle to the request
- */
- @Override
- public CancellableTask getIconInBackground(Task task, @NonNull GetTaskIconCallback callback) {
- Preconditions.assertUIThread();
- if (task.icon != null) {
- // Nothing to load, the icon is already loaded
- callback.onTaskIconReceived(task.icon, task.titleDescription, task.title);
- return null;
- }
- CancellableTask<TaskCacheEntry> request = new CancellableTask<>(
- () -> getCacheEntry(task),
- MAIN_EXECUTOR,
- result -> {
- task.icon = result.icon;
- task.titleDescription = result.contentDescription;
- task.title = result.title;
-
- callback.onTaskIconReceived(
- result.icon,
- result.contentDescription,
- result.title);
- dispatchIconUpdate(task.key.id);
- }
- );
- mBgExecutor.execute(request);
- return request;
- }
-
- /**
- * Clears the icon cache
- */
- public void clearCache() {
- mBgExecutor.execute(this::resetFactory);
- }
-
- void onTaskRemoved(TaskKey taskKey) {
- mIconCache.remove(taskKey);
- }
-
- void invalidateCacheEntries(String pkg, UserHandle handle) {
- mBgExecutor.execute(() -> mIconCache.removeAll(key ->
- pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId));
- }
-
- @WorkerThread
- private TaskCacheEntry getCacheEntry(Task task) {
- TaskCacheEntry entry = mIconCache.getAndInvalidateIfModified(task.key);
- if (entry != null) {
- return entry;
- }
-
- TaskDescription desc = task.taskDescription;
- TaskKey key = task.key;
- ActivityInfo activityInfo = null;
-
- // Create new cache entry
- entry = new TaskCacheEntry();
-
- // Load icon
- // TODO: Load icon resource (b/143363444)
- Bitmap icon = getIcon(desc, key.userId);
- if (icon != null) {
- entry.icon = getBitmapInfo(
- new BitmapDrawable(mContext.getResources(), icon),
- key.userId,
- desc.getPrimaryColor(),
- false /* isInstantApp */).newIcon(mContext);
- } else {
- activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
- key.getComponent(), key.userId);
- if (activityInfo != null) {
- BitmapInfo bitmapInfo = getBitmapInfo(
- mIconProvider.getIcon(activityInfo),
- key.userId,
- desc.getPrimaryColor(),
- activityInfo.applicationInfo.isInstantApp());
- entry.icon = bitmapInfo.newIcon(mContext);
- } else {
- entry.icon = getDefaultIcon(key.userId);
- }
- }
-
- // Skip loading the content description if the activity no longer exists
- if (activityInfo == null) {
- activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
- key.getComponent(), key.userId);
- }
- if (activityInfo != null) {
- entry.contentDescription = getBadgedContentDescription(
- activityInfo, task.key.userId, task.taskDescription);
- if (enableOverviewIconMenu()) {
- entry.title = Utilities.trim(activityInfo.loadLabel(mContext.getPackageManager()));
- }
- }
-
- mIconCache.put(task.key, entry);
- return entry;
- }
-
- private Bitmap getIcon(ActivityManager.TaskDescription desc, int userId) {
- if (desc.getInMemoryIcon() != null) {
- return desc.getInMemoryIcon();
- }
- return ActivityManager.TaskDescription.loadTaskDescriptionIcon(
- desc.getIconFilename(), userId);
- }
-
- private String getBadgedContentDescription(ActivityInfo info, int userId, TaskDescription td) {
- PackageManager pm = mContext.getPackageManager();
- String taskLabel = td == null ? null : Utilities.trim(td.getLabel());
- if (TextUtils.isEmpty(taskLabel)) {
- taskLabel = Utilities.trim(info.loadLabel(pm));
- }
-
- String applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(pm));
- String badgedApplicationLabel = userId != UserHandle.myUserId()
- ? pm.getUserBadgedLabel(applicationLabel, UserHandle.of(userId)).toString()
- : applicationLabel;
- return applicationLabel.equals(taskLabel)
- ? badgedApplicationLabel : badgedApplicationLabel + " " + taskLabel;
- }
-
- @WorkerThread
- private Drawable getDefaultIcon(int userId) {
- synchronized (mDefaultIcons) {
- if (mDefaultIconBase == null) {
- try (BaseIconFactory bif = getIconFactory()) {
- mDefaultIconBase = bif.makeDefaultIcon(mIconProvider);
- }
- }
-
- int index;
- if ((index = mDefaultIcons.indexOfKey(userId)) >= 0) {
- return mDefaultIcons.valueAt(index).newIcon(mContext);
- } else {
- BitmapInfo info = mDefaultIconBase.withFlags(
- UserCache.INSTANCE.get(mContext).getUserInfo(UserHandle.of(userId))
- .applyBitmapInfoFlags(FlagOp.NO_OP));
- mDefaultIcons.put(userId, info);
- return info.newIcon(mContext);
- }
- }
- }
-
- @WorkerThread
- private BitmapInfo getBitmapInfo(Drawable drawable, int userId,
- int primaryColor, boolean isInstantApp) {
- try (BaseIconFactory bif = getIconFactory()) {
- bif.setWrapperBackgroundColor(primaryColor);
-
- // User version code O, so that the icon is always wrapped in an adaptive icon container
- return bif.createBadgedIconBitmap(drawable,
- new IconOptions()
- .setUser(UserCache.INSTANCE.get(mContext)
- .getUserInfo(UserHandle.of(userId)))
- .setInstantApp(isInstantApp)
- .setExtractedColor(0));
- }
- }
-
- @WorkerThread
- private BaseIconFactory getIconFactory() {
- if (mIconFactory == null) {
- mIconFactory = new BaseIconFactory(mContext,
- DisplayController.INSTANCE.get(mContext).getInfo().getDensityDpi(),
- mContext.getResources().getDimensionPixelSize(
- R.dimen.task_icon_cache_default_icon_size));
- }
- return mIconFactory;
- }
-
- @WorkerThread
- private void resetFactory() {
- mIconFactory = null;
- mIconCache.evictAll();
- }
-
- private static class TaskCacheEntry {
- public Drawable icon;
- public String contentDescription = "";
- public String title = "";
- }
-
- /** Callback used when retrieving app icons from cache. */
- public interface GetTaskIconCallback {
- /** Called when task icon is retrieved. */
- void onTaskIconReceived(Drawable icon, String contentDescription, String title);
- }
-
- void registerTaskVisualsChangeListener(TaskVisualsChangeListener newListener) {
- mTaskVisualsChangeListener = newListener;
- }
-
- void removeTaskVisualsChangeListener() {
- mTaskVisualsChangeListener = null;
- }
-
- void dispatchIconUpdate(int taskId) {
- if (mTaskVisualsChangeListener != null) {
- mTaskVisualsChangeListener.onTaskIconChanged(taskId);
- }
- }
-}
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.kt b/quickstep/src/com/android/quickstep/TaskIconCache.kt
new file mode 100644
index 0000000..bf94d41
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.kt
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.util.SparseArray
+import androidx.annotation.WorkerThread
+import com.android.launcher3.Flags.enableOverviewIconMenu
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.icons.BaseIconFactory
+import com.android.launcher3.icons.BaseIconFactory.IconOptions
+import com.android.launcher3.icons.BitmapInfo
+import com.android.launcher3.icons.IconProvider
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.CancellableTask
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.FlagOp
+import com.android.launcher3.util.Preconditions
+import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
+import com.android.quickstep.util.TaskKeyLruCache
+import com.android.quickstep.util.TaskVisualsChangeListener
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.systemui.shared.system.PackageManagerWrapper
+import java.util.concurrent.Executor
+
+/** Manages the caching of task icons and related data. */
+class TaskIconCache(
+ private val context: Context,
+ private val bgExecutor: Executor,
+ private val iconProvider: IconProvider,
+) : TaskIconDataSource, DisplayInfoChangeListener {
+ private val iconCache =
+ TaskKeyLruCache<TaskCacheEntry>(
+ context.resources.getInteger(R.integer.recentsIconCacheSize)
+ )
+ private val defaultIcons = SparseArray<BitmapInfo>()
+ private var defaultIconBase: BitmapInfo? = null
+
+ private var _iconFactory: BaseIconFactory? = null
+ @get:WorkerThread
+ private val iconFactory: BaseIconFactory
+ get() =
+ if (enableRefactorTaskThumbnail()) createIconFactory()
+ else _iconFactory ?: createIconFactory().also { _iconFactory = it }
+
+ var taskVisualsChangeListener: TaskVisualsChangeListener? = null
+
+ init {
+ DisplayController.INSTANCE.get(context).addChangeListener(this)
+ }
+
+ override fun onDisplayInfoChanged(context: Context, info: DisplayController.Info, flags: Int) {
+ if ((flags and DisplayController.CHANGE_DENSITY) != 0) {
+ clearCache()
+ }
+ }
+
+ // TODO(b/387496731): Add ensureActive() calls if they show performance benefit
+ override suspend fun getIcon(task: Task): TaskCacheEntry {
+ task.icon?.let {
+ // Nothing to load, the icon is already loaded
+ return TaskCacheEntry(it, task.titleDescription ?: "", task.title)
+ }
+
+ val entry = getCacheEntry(task)
+ task.icon = entry.icon
+ task.titleDescription = entry.contentDescription
+ task.title = entry.title
+
+ dispatchIconUpdate(task.key.id)
+ return entry
+ }
+
+ /**
+ * Asynchronously fetches the icon and other task data.
+ *
+ * @param task The task to fetch the data for
+ * @param callback The callback to receive the task after its data has been populated.
+ * @return A cancelable handle to the request
+ */
+ fun getIconInBackground(task: Task, callback: GetTaskIconCallback): CancellableTask<*>? {
+ Preconditions.assertUIThread()
+ task.icon?.let {
+ // Nothing to load, the icon is already loaded
+ callback.onTaskIconReceived(it, task.titleDescription ?: "", task.title ?: "")
+ return null
+ }
+ val request =
+ CancellableTask(
+ { getCacheEntry(task) },
+ Executors.MAIN_EXECUTOR,
+ { result: TaskCacheEntry ->
+ task.icon = result.icon
+ task.titleDescription = result.contentDescription
+ task.title = result.title
+
+ callback.onTaskIconReceived(
+ result.icon,
+ result.contentDescription,
+ result.title,
+ )
+ dispatchIconUpdate(task.key.id)
+ },
+ )
+ bgExecutor.execute(request)
+ return request
+ }
+
+ /** Clears the icon cache */
+ fun clearCache() {
+ bgExecutor.execute { resetFactory() }
+ }
+
+ fun onTaskRemoved(taskKey: TaskKey) {
+ iconCache.remove(taskKey)
+ }
+
+ fun invalidateCacheEntries(pkg: String, handle: UserHandle) {
+ bgExecutor.execute {
+ iconCache.removeAll { key: TaskKey ->
+ pkg == key.packageName && handle.identifier == key.userId
+ }
+ }
+ }
+
+ @WorkerThread
+ private fun createIconFactory() =
+ BaseIconFactory(
+ context,
+ DisplayController.INSTANCE.get(context).info.densityDpi,
+ context.resources.getDimensionPixelSize(R.dimen.task_icon_cache_default_icon_size),
+ )
+
+ @WorkerThread
+ private fun getCacheEntry(task: Task): TaskCacheEntry {
+ iconCache.getAndInvalidateIfModified(task.key)?.let {
+ return it
+ }
+
+ val desc = task.taskDescription
+ val key = task.key
+ var activityInfo: ActivityInfo? = null
+
+ // Create new cache entry
+
+ // Load icon
+ val icon = getIcon(desc, key.userId)
+ val entryIcon =
+ if (icon != null) {
+ getBitmapInfo(
+ BitmapDrawable(context.resources, icon),
+ key.userId,
+ desc.primaryColor,
+ false, /* isInstantApp */
+ )
+ .newIcon(context)
+ } else {
+ activityInfo =
+ PackageManagerWrapper.getInstance().getActivityInfo(key.component, key.userId)
+ if (activityInfo != null) {
+ val bitmapInfo =
+ getBitmapInfo(
+ iconProvider.getIcon(activityInfo),
+ key.userId,
+ desc.primaryColor,
+ activityInfo.applicationInfo.isInstantApp,
+ )
+ bitmapInfo.newIcon(context)
+ } else {
+ getDefaultIcon(key.userId)
+ }
+ }
+
+ activityInfo =
+ activityInfo
+ ?: PackageManagerWrapper.getInstance().getActivityInfo(key.component, key.userId)
+
+ return when {
+ // Skip loading the content description if the activity no longer exists
+ activityInfo == null -> TaskCacheEntry(entryIcon)
+ enableOverviewIconMenu() ->
+ TaskCacheEntry(
+ entryIcon,
+ getBadgedContentDescription(
+ activityInfo,
+ task.key.userId,
+ task.taskDescription,
+ ),
+ Utilities.trim(activityInfo.loadLabel(context.packageManager)),
+ )
+ else ->
+ TaskCacheEntry(
+ entryIcon,
+ getBadgedContentDescription(activityInfo, task.key.userId, task.taskDescription),
+ )
+ }.also { iconCache.put(task.key, it) }
+ }
+
+ private fun getIcon(desc: ActivityManager.TaskDescription, userId: Int): Bitmap? =
+ desc.inMemoryIcon
+ ?: ActivityManager.TaskDescription.loadTaskDescriptionIcon(desc.iconFilename, userId)
+
+ private fun getBadgedContentDescription(
+ info: ActivityInfo,
+ userId: Int,
+ taskDescription: ActivityManager.TaskDescription?,
+ ): String {
+ val packageManager = context.packageManager
+ var taskLabel = taskDescription?.let { Utilities.trim(it.label) }
+ if (taskLabel.isNullOrEmpty()) {
+ taskLabel = Utilities.trim(info.loadLabel(packageManager))
+ }
+
+ val applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(packageManager))
+ val badgedApplicationLabel =
+ if (userId != UserHandle.myUserId())
+ packageManager
+ .getUserBadgedLabel(applicationLabel, UserHandle.of(userId))
+ .toString()
+ else applicationLabel
+ return if (applicationLabel == taskLabel) badgedApplicationLabel
+ else "$badgedApplicationLabel $taskLabel"
+ }
+
+ @WorkerThread
+ private fun getDefaultIcon(userId: Int): Drawable {
+ synchronized(defaultIcons) {
+ val defaultIconBase =
+ defaultIconBase ?: iconFactory.use { it.makeDefaultIcon(iconProvider) }
+ val index: Int = defaultIcons.indexOfKey(userId)
+ return if (index >= 0) {
+ defaultIcons.valueAt(index).newIcon(context)
+ } else {
+ val info =
+ defaultIconBase.withFlags(
+ UserCache.INSTANCE.get(context)
+ .getUserInfo(UserHandle.of(userId))
+ .applyBitmapInfoFlags(FlagOp.NO_OP)
+ )
+ defaultIcons.put(userId, info)
+ info.newIcon(context)
+ }
+ }
+ }
+
+ @WorkerThread
+ private fun getBitmapInfo(
+ drawable: Drawable,
+ userId: Int,
+ primaryColor: Int,
+ isInstantApp: Boolean,
+ ): BitmapInfo {
+ iconFactory.use { iconFactory ->
+ iconFactory.setWrapperBackgroundColor(primaryColor)
+ // User version code O, so that the icon is always wrapped in an adaptive icon container
+ return iconFactory.createBadgedIconBitmap(
+ drawable,
+ IconOptions()
+ .setUser(UserCache.INSTANCE.get(context).getUserInfo(UserHandle.of(userId)))
+ .setInstantApp(isInstantApp)
+ .setExtractedColor(0),
+ )
+ }
+ }
+
+ @WorkerThread
+ private fun resetFactory() {
+ _iconFactory = null
+ iconCache.evictAll()
+ }
+
+ data class TaskCacheEntry(
+ val icon: Drawable,
+ val contentDescription: String = "",
+ val title: String = "",
+ )
+
+ /** Callback used when retrieving app icons from cache. */
+ fun interface GetTaskIconCallback {
+ /** Called when task icon is retrieved. */
+ fun onTaskIconReceived(icon: Drawable, contentDescription: String, title: String)
+ }
+
+ fun registerTaskVisualsChangeListener(newListener: TaskVisualsChangeListener?) {
+ taskVisualsChangeListener = newListener
+ }
+
+ fun removeTaskVisualsChangeListener() {
+ taskVisualsChangeListener = null
+ }
+
+ private fun dispatchIconUpdate(taskId: Int) {
+ taskVisualsChangeListener?.onTaskIconChanged(taskId)
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
deleted file mode 100644
index 580dcc2..0000000
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.Flags.enableGridOnlyOverview;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.content.Context;
-import android.content.res.Resources;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.CancellableTask;
-import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.recents.data.HighResLoadingStateNotifier;
-import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource;
-import com.android.quickstep.util.TaskKeyByLastActiveTimeCache;
-import com.android.quickstep.util.TaskKeyCache;
-import com.android.quickstep.util.TaskKeyLruCache;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-
-import java.util.ArrayList;
-import java.util.concurrent.Executor;
-import java.util.function.Consumer;
-
-public class TaskThumbnailCache implements TaskThumbnailDataSource {
-
- private final Executor mBgExecutor;
- private final TaskKeyCache<ThumbnailData> mCache;
- private final HighResLoadingState mHighResLoadingState;
- private final boolean mEnableTaskSnapshotPreloading;
- private final Context mContext;
-
- public static class HighResLoadingState implements HighResLoadingStateNotifier {
- private boolean mForceHighResThumbnails;
- private boolean mVisible;
- private boolean mFlingingFast;
- private boolean mHighResLoadingEnabled;
- private ArrayList<HighResLoadingStateChangedCallback> mCallbacks = new ArrayList<>();
-
- public interface HighResLoadingStateChangedCallback {
- void onHighResLoadingStateChanged(boolean enabled);
- }
-
- private HighResLoadingState(Context context) {
- // If the device does not support low-res thumbnails, only attempt to load high-res
- // thumbnails
- mForceHighResThumbnails = !supportsLowResThumbnails();
- }
-
- @Override
- public void addCallback(@NonNull HighResLoadingStateChangedCallback callback) {
- mCallbacks.add(callback);
- }
-
- @Override
- public void removeCallback(@NonNull HighResLoadingStateChangedCallback callback) {
- mCallbacks.remove(callback);
- }
-
- public void setVisible(boolean visible) {
- mVisible = visible;
- updateState();
- }
-
- public void setFlingingFast(boolean flingingFast) {
- mFlingingFast = flingingFast;
- updateState();
- }
-
- public boolean isEnabled() {
- return mHighResLoadingEnabled;
- }
-
- private void updateState() {
- boolean prevState = mHighResLoadingEnabled;
- mHighResLoadingEnabled = mForceHighResThumbnails || (mVisible && !mFlingingFast);
- if (prevState != mHighResLoadingEnabled) {
- for (int i = mCallbacks.size() - 1; i >= 0; i--) {
- mCallbacks.get(i).onHighResLoadingStateChanged(mHighResLoadingEnabled);
- }
- }
- }
- }
-
- public TaskThumbnailCache(Context context, Executor bgExecutor) {
- this(context, bgExecutor,
- context.getResources().getInteger(R.integer.recentsThumbnailCacheSize));
- }
-
- private TaskThumbnailCache(Context context, Executor bgExecutor, int cacheSize) {
- this(context, bgExecutor,
- enableGridOnlyOverview() ? new TaskKeyByLastActiveTimeCache<>(cacheSize)
- : new TaskKeyLruCache<>(cacheSize));
- }
-
- @VisibleForTesting
- TaskThumbnailCache(Context context, Executor bgExecutor, TaskKeyCache<ThumbnailData> cache) {
- mBgExecutor = bgExecutor;
- mHighResLoadingState = new HighResLoadingState(context);
- mContext = context;
-
- Resources res = context.getResources();
- mEnableTaskSnapshotPreloading = res.getBoolean(R.bool.config_enableTaskSnapshotPreloading);
- mCache = cache;
- }
-
- /**
- * Synchronously fetches the thumbnail for the given task at the specified resolution level, and
- * puts it in the cache.
- */
- public void updateThumbnailInCache(Task task, boolean lowResolution) {
- if (task == null) {
- return;
- }
- Preconditions.assertUIThread();
- // Fetch the thumbnail for this task and put it in the cache
- if (task.thumbnail == null) {
- getThumbnailInBackground(task.key, lowResolution, t -> task.thumbnail = t);
- }
- }
-
- /**
- * Synchronously updates the thumbnail in the cache if it is already there.
- */
- public void updateTaskSnapShot(int taskId, ThumbnailData thumbnail) {
- Preconditions.assertUIThread();
- mCache.updateIfAlreadyInCache(taskId, thumbnail);
- }
-
- /**
- * Asynchronously fetches the thumbnail for the given {@code task}.
- *
- * @param callback The callback to receive the task after its data has been populated.
- * @return A cancelable handle to the request
- */
- @Override
- public CancellableTask<ThumbnailData> getThumbnailInBackground(
- Task task, @NonNull Consumer<ThumbnailData> callback) {
- Preconditions.assertUIThread();
-
- boolean lowResolution = !mHighResLoadingState.isEnabled();
- if (task.thumbnail != null && task.thumbnail.getThumbnail() != null
- && (!task.thumbnail.reducedResolution || lowResolution)) {
- // Nothing to load, the thumbnail is already high-resolution or matches what the
- // request, so just callback
- callback.accept(task.thumbnail);
- return null;
- }
-
- return getThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), callback);
- }
-
- /**
- * Updates cache size and remove excess entries if current size is more than new cache size.
- *
- * @return whether cache size has increased
- */
- public boolean updateCacheSizeAndRemoveExcess() {
- int newSize = mContext.getResources().getInteger(R.integer.recentsThumbnailCacheSize);
- int oldSize = mCache.getMaxSize();
- if (newSize == oldSize) {
- // Return if no change in size
- return false;
- }
-
- mCache.updateCacheSizeAndRemoveExcess(newSize);
- return newSize > oldSize;
- }
-
- private CancellableTask<ThumbnailData> getThumbnailInBackground(TaskKey key,
- boolean lowResolution, Consumer<ThumbnailData> callback) {
- Preconditions.assertUIThread();
-
- ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
- if (cachedThumbnail != null && cachedThumbnail.getThumbnail() != null
- && (!cachedThumbnail.reducedResolution || lowResolution)) {
- // Already cached, lets use that thumbnail
- callback.accept(cachedThumbnail);
- return null;
- }
-
- CancellableTask<ThumbnailData> request = new CancellableTask<>(
- () -> {
- ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance()
- .getTaskThumbnail(key.id, lowResolution);
- return thumbnailData.getThumbnail() != null ? thumbnailData
- : ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id);
- },
- MAIN_EXECUTOR,
- result -> {
- // Avoid an async timing issue that a low res entry replaces an existing high
- // res entry in high res enabled state, so we check before putting it to cache
- if (enableGridOnlyOverview() && result.reducedResolution
- && getHighResLoadingState().isEnabled()) {
- ThumbnailData newCachedThumbnail = mCache.getAndInvalidateIfModified(key);
- if (newCachedThumbnail != null && newCachedThumbnail.getThumbnail() != null
- && !newCachedThumbnail.reducedResolution) {
- return;
- }
- }
- mCache.put(key, result);
- callback.accept(result);
- }
- );
- mBgExecutor.execute(request);
- return request;
- }
-
- /**
- * Clears the cache.
- */
- public void clear() {
- mCache.evictAll();
- }
-
- /**
- * Removes the cached thumbnail for the given task.
- */
- public void remove(Task.TaskKey key) {
- mCache.remove(key);
- }
-
- /**
- * @return The cache size.
- */
- public int getCacheSize() {
- return mCache.getMaxSize();
- }
-
- /**
- * @return The mutable high-res loading state.
- */
- public HighResLoadingState getHighResLoadingState() {
- return mHighResLoadingState;
- }
-
- /**
- * @return Whether to enable background preloading of task thumbnails.
- */
- public boolean isPreloadingEnabled() {
- return mEnableTaskSnapshotPreloading && mHighResLoadingState.mVisible;
- }
-
- /**
- * @return Whether device supports low-res thumbnails. Low-res files are an optimization
- * for faster load times of snapshots. Devices can optionally disable low-res files so that
- * they only store snapshots at high-res scale. The actual scale can be configured in
- * frameworks/base config overlay.
- */
- private static boolean supportsLowResThumbnails() {
- Resources res = Resources.getSystem();
- int resId = res.getIdentifier("config_lowResTaskSnapshotScale", "dimen", "android");
- if (resId != 0) {
- return 0 < res.getFloat(resId);
- }
- return true;
- }
-
-}
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.kt b/quickstep/src/com/android/quickstep/TaskThumbnailCache.kt
new file mode 100644
index 0000000..7b56213
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import com.android.launcher3.Flags.enableGridOnlyOverview
+import com.android.launcher3.R
+import com.android.launcher3.util.CancellableTask
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.Preconditions
+import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
+import com.android.quickstep.util.TaskKeyByLastActiveTimeCache
+import com.android.quickstep.util.TaskKeyCache
+import com.android.quickstep.util.TaskKeyLruCache
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+
+class TaskThumbnailCache
+@VisibleForTesting
+internal constructor(
+ private val context: Context,
+ private val bgExecutor: Executor,
+ private val cache: TaskKeyCache<ThumbnailData>,
+) : TaskThumbnailDataSource {
+ val highResLoadingState = HighResLoadingState()
+ private val enableTaskSnapshotPreloading =
+ context.resources.getBoolean(R.bool.config_enableTaskSnapshotPreloading)
+
+ @JvmOverloads
+ constructor(
+ context: Context,
+ bgExecutor: Executor,
+ cacheSize: Int = context.resources.getInteger(R.integer.recentsThumbnailCacheSize),
+ ) : this(
+ context,
+ bgExecutor,
+ if (enableGridOnlyOverview()) TaskKeyByLastActiveTimeCache(cacheSize)
+ else TaskKeyLruCache(cacheSize),
+ )
+
+ /**
+ * Synchronously fetches the thumbnail for the given task at the specified resolution level, and
+ * puts it in the cache.
+ */
+ fun updateThumbnailInCache(task: Task?, lowResolution: Boolean) {
+ task ?: return
+
+ Preconditions.assertUIThread()
+ // Fetch the thumbnail for this task and put it in the cache
+ if (task.thumbnail == null) {
+ getThumbnailInBackground(task.key, lowResolution) { t: ThumbnailData? ->
+ task.thumbnail = t
+ }
+ }
+ }
+
+ /** Synchronously updates the thumbnail in the cache if it is already there. */
+ fun updateTaskSnapShot(taskId: Int, thumbnail: ThumbnailData?) {
+ Preconditions.assertUIThread()
+ cache.updateIfAlreadyInCache(taskId, thumbnail)
+ }
+
+ // TODO(b/387496731): Add ensureActive() calls if they show performance benefit
+ /**
+ * Retrieves a thumbnail for the provided `task` on the current thread. This should not be
+ * called from the main thread.
+ */
+ @WorkerThread
+ override suspend fun getThumbnail(task: Task): ThumbnailData? {
+ val lowResolution: Boolean = !highResLoadingState.isEnabled
+ // Check task for thumbnail
+ val taskThumbnail: ThumbnailData? = task.thumbnail
+ if (
+ taskThumbnail?.thumbnail != null && (!taskThumbnail.reducedResolution || lowResolution)
+ ) {
+ return taskThumbnail
+ }
+
+ // Check cache for thumbnail
+ val cachedThumbnail: ThumbnailData? = cache.getAndInvalidateIfModified(task.key)
+ if (
+ cachedThumbnail?.thumbnail != null &&
+ (!cachedThumbnail.reducedResolution || lowResolution)
+ ) {
+ return cachedThumbnail
+ }
+
+ // Get thumbnail from system
+ var thumbnailData =
+ ActivityManagerWrapper.getInstance().getTaskThumbnail(task.key.id, lowResolution)
+ if (thumbnailData.thumbnail == null) {
+ thumbnailData = ActivityManagerWrapper.getInstance().takeTaskThumbnail(task.key.id)
+ }
+
+ // Avoid an async timing issue that a low res entry replaces an existing high
+ // res entry in high res enabled state, so we check before putting it to cache
+ if (
+ enableGridOnlyOverview() &&
+ thumbnailData.reducedResolution &&
+ highResLoadingState.isEnabled
+ ) {
+ val newCachedThumbnail = cache.getAndInvalidateIfModified(task.key)
+ if (newCachedThumbnail.thumbnail != null && !newCachedThumbnail.reducedResolution) {
+ return newCachedThumbnail
+ }
+ }
+ cache.put(task.key, thumbnailData)
+ return thumbnailData
+ }
+
+ /**
+ * Asynchronously fetches the thumbnail for the given `task`.
+ *
+ * @param callback The callback to receive the task after its data has been populated.
+ * @return a cancelable handle to the request
+ */
+ fun getThumbnailInBackground(
+ task: Task,
+ callback: Consumer<ThumbnailData>,
+ ): CancellableTask<ThumbnailData>? {
+ Preconditions.assertUIThread()
+
+ val lowResolution = !highResLoadingState.isEnabled
+ val taskThumbnail = task.thumbnail
+ if (
+ taskThumbnail?.thumbnail != null && (!taskThumbnail.reducedResolution || lowResolution)
+ ) {
+ // Nothing to load, the thumbnail is already high-resolution or matches what the
+ // request, so just callback
+ callback.accept(taskThumbnail)
+ return null
+ }
+
+ return getThumbnailInBackground(task.key, !highResLoadingState.isEnabled, callback)
+ }
+
+ /**
+ * Updates cache size and remove excess entries if current size is more than new cache size.
+ *
+ * @return whether cache size has increased
+ */
+ fun updateCacheSizeAndRemoveExcess(): Boolean {
+ val newSize = context.resources.getInteger(R.integer.recentsThumbnailCacheSize)
+ val oldSize = cache.maxSize
+ if (newSize == oldSize) {
+ // Return if no change in size
+ return false
+ }
+
+ cache.updateCacheSizeAndRemoveExcess(newSize)
+ return newSize > oldSize
+ }
+
+ private fun getThumbnailInBackground(
+ key: TaskKey,
+ lowResolution: Boolean,
+ callback: Consumer<ThumbnailData>,
+ ): CancellableTask<ThumbnailData>? {
+ Preconditions.assertUIThread()
+
+ val cachedThumbnail = cache.getAndInvalidateIfModified(key)
+ if (
+ cachedThumbnail?.thumbnail != null &&
+ (!cachedThumbnail.reducedResolution || lowResolution)
+ ) {
+ // Already cached, lets use that thumbnail
+ callback.accept(cachedThumbnail)
+ return null
+ }
+
+ val request =
+ CancellableTask(
+ {
+ val thumbnailData =
+ ActivityManagerWrapper.getInstance().getTaskThumbnail(key.id, lowResolution)
+ if (thumbnailData.thumbnail != null) thumbnailData
+ else ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id)
+ },
+ Executors.MAIN_EXECUTOR,
+ Consumer { result: ThumbnailData ->
+ // Avoid an async timing issue that a low res entry replaces an existing high
+ // res entry in high res enabled state, so we check before putting it to cache
+ if (
+ enableGridOnlyOverview() &&
+ result.reducedResolution &&
+ highResLoadingState.isEnabled
+ ) {
+ val newCachedThumbnail = cache.getAndInvalidateIfModified(key)
+ if (
+ newCachedThumbnail?.thumbnail != null &&
+ !newCachedThumbnail.reducedResolution
+ ) {
+ return@Consumer
+ }
+ }
+ cache.put(key, result)
+ callback.accept(result)
+ },
+ )
+ bgExecutor.execute(request)
+ return request
+ }
+
+ /** Clears the cache. */
+ fun clear() {
+ cache.evictAll()
+ }
+
+ /** Removes the cached thumbnail for the given task. */
+ fun remove(key: TaskKey) {
+ cache.remove(key)
+ }
+
+ /** Returns The cache size. */
+ fun getCacheSize() = cache.maxSize
+
+ /** Returns Whether to enable background preloading of task thumbnails. */
+ fun isPreloadingEnabled() = enableTaskSnapshotPreloading && highResLoadingState.visible
+}
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index daac9fb..44fdaec 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -136,7 +136,7 @@
setter.add(pa.buildAnim());
}
- Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
+ Pair<FloatProperty<RecentsView<?, ?>>, FloatProperty<RecentsView<?, ?>>> taskViewsFloat =
mRecentsView.getPagedOrientationHandler().getSplitSelectTaskOffset(
TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
mRecentsViewContainer.getDeviceProfile());
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
index 6b61298..f3f73c0 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
@@ -53,7 +53,7 @@
private final int mTouchSlop;
private final PointF mDownPos = new PointF();
private final PointF mLastPos = new PointF();
- private final long mTimeForTap;
+ private final long mTimeForLongPress;
private int mActivePointerId = INVALID_POINTER_ID;
public BubbleBarInputConsumer(Context context, BubbleControllers bubbleControllers,
@@ -64,7 +64,7 @@
mInputMonitorCompat = inputMonitorCompat;
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
- mTimeForTap = ViewConfiguration.getTapTimeout();
+ mTimeForLongPress = ViewConfiguration.getLongPressTimeout();
}
@Override
@@ -110,7 +110,8 @@
case MotionEvent.ACTION_UP:
boolean swipeUpOnBubbleHandle = mBubbleBarSwipeController != null
&& mBubbleBarSwipeController.isSwipeGesture();
- boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForTap;
+ // Anything less than a long-press is a tap
+ boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForLongPress;
if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop
&& mStashedOrCollapsedOnDown) {
// Taps on the handle / collapsed state should open the bar
diff --git a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
index df546ca..ad2bd25 100644
--- a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
@@ -16,7 +16,7 @@
package com.android.quickstep.recents.data
-import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
/** Notifies added callbacks that high res state has changed */
interface HighResLoadingStateNotifier {
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
index a45d194..608fafd 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
@@ -17,7 +17,7 @@
package com.android.quickstep.recents.data
import android.os.UserHandle
-import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
import com.android.quickstep.util.TaskVisualsChangeListener
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 8a1b211..703d631 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -17,6 +17,7 @@
package com.android.quickstep.recents.data
import android.graphics.drawable.Drawable
+import android.graphics.drawable.ShapeDrawable
import android.util.Log
import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
@@ -25,16 +26,16 @@
import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
-import kotlin.coroutines.resume
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
class TasksRepository(
@@ -112,10 +113,11 @@
taskRequests[taskId] =
Pair(
task.key,
- recentsCoroutineScope.launch(dispatcherProvider.main) {
+ recentsCoroutineScope.launch(dispatcherProvider.background) {
Log.i(TAG, "requestTaskData: $taskId")
- fetchIcon(task)
- fetchThumbnail(task)
+ val thumbnailFetchDeferred = async { fetchThumbnail(task) }
+ val iconFetchDeferred = async { fetchIcon(task) }
+ awaitAll(thumbnailFetchDeferred, iconFetchDeferred)
},
)
}
@@ -150,7 +152,7 @@
task.key,
object : TaskIconChangedCallback {
override fun onTaskIconChanged() {
- recentsCoroutineScope.launch(dispatcherProvider.main) {
+ recentsCoroutineScope.launch(dispatcherProvider.background) {
updateIcon(task.key.id, getIconFromDataSource(task))
}
}
@@ -168,7 +170,7 @@
}
override fun onHighResLoadingStateChanged() {
- recentsCoroutineScope.launch(dispatcherProvider.main) {
+ recentsCoroutineScope.launch(dispatcherProvider.background) {
updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
}
}
@@ -191,34 +193,18 @@
}
private suspend fun getThumbnailFromDataSource(task: Task) =
- withContext(dispatcherProvider.main) {
- suspendCancellableCoroutine { continuation ->
- val cancellableTask =
- taskThumbnailDataSource.getThumbnailInBackground(task) {
- continuation.resume(it)
- }
- continuation.invokeOnCancellation { cancellableTask?.cancel() }
- }
- }
+ withContext(dispatcherProvider.background) { taskThumbnailDataSource.getThumbnail(task) }
private suspend fun getIconFromDataSource(task: Task) =
- withContext(dispatcherProvider.main) {
- suspendCancellableCoroutine { continuation ->
- val cancellableTask =
- taskIconDataSource.getIconInBackground(task) { icon, contentDescription, title
- ->
- icon.constantState?.let {
- continuation.resume(
- IconData(it.newDrawable().mutate(), contentDescription, title)
- )
- }
- }
- continuation.invokeOnCancellation { cancellableTask?.cancel() }
- }
+ withContext(dispatcherProvider.background) {
+ val iconCacheEntry = taskIconDataSource.getIcon(task)
+ val icon = iconCacheEntry.icon.constantState?.newDrawable()?.mutate() ?: EMPTY_DRAWABLE
+ IconData(icon, iconCacheEntry.contentDescription, iconCacheEntry.title)
}
companion object {
private const val TAG = "TasksRepository"
+ private val EMPTY_DRAWABLE = ShapeDrawable()
}
/** Helper class to support StateFlow emissions when using a Map with a MutableStateFlow. */
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 9d8fc4f..dd83af6 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -191,6 +191,7 @@
recentsViewData = inject(),
recentTasksRepository = inject(),
getThumbnailPositionUseCase = inject(),
+ dispatcherProvider = inject(),
)
}
GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
index ab699c6..c45458c 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
@@ -16,10 +16,9 @@
package com.android.quickstep.task.thumbnail.data
-import com.android.launcher3.util.CancellableTask
-import com.android.quickstep.TaskIconCache.GetTaskIconCallback
+import com.android.quickstep.TaskIconCache
import com.android.systemui.shared.recents.model.Task
interface TaskIconDataSource {
- fun getIconInBackground(task: Task, callback: GetTaskIconCallback): CancellableTask<*>?
+ suspend fun getIcon(task: Task): TaskIconCache.TaskCacheEntry
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
index 986acbe..6e63ea9 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
@@ -16,14 +16,9 @@
package com.android.quickstep.task.thumbnail.data
-import com.android.launcher3.util.CancellableTask
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
-import java.util.function.Consumer
interface TaskThumbnailDataSource {
- fun getThumbnailInBackground(
- task: Task,
- callback: Consumer<ThumbnailData>
- ): CancellableTask<ThumbnailData>?
+ suspend fun getThumbnail(task: Task): ThumbnailData?
}
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
index e6c8d27..0f61b95 100644
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -74,6 +74,7 @@
recentsViewData = RecentsDependencies.get(),
getThumbnailPositionUseCase = RecentsDependencies.get(),
recentTasksRepository = RecentsDependencies.get(),
+ dispatcherProvider = RecentsDependencies.get(),
)
viewModel.overlayState
.onEach {
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
index 14359db..81a904b 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -17,6 +17,7 @@
package com.android.quickstep.task.viewmodel
import android.graphics.Matrix
+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.MatrixScaling
@@ -27,6 +28,7 @@
import com.android.systemui.shared.recents.model.Task
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
/** View model for TaskOverlay */
@@ -35,11 +37,14 @@
recentsViewData: RecentsViewData,
private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
recentTasksRepository: RecentTasksRepository,
+ dispatcherProvider: DispatcherProvider,
) {
val overlayState =
combine(
recentsViewData.overlayEnabled,
- recentsViewData.settledFullyVisibleTaskIds.map { it.contains(task.key.id) },
+ recentsViewData.settledFullyVisibleTaskIds
+ .map { it.contains(task.key.id) }
+ .distinctUntilChanged(),
recentTasksRepository.getThumbnailById(task.key.id),
) { isOverlayEnabled, isFullyVisible, thumbnailData ->
if (isOverlayEnabled && isFullyVisible) {
@@ -52,6 +57,7 @@
}
}
.distinctUntilChanged()
+ .flowOn(dispatcherProvider.background)
fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
val matrix: Matrix
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 4660c51..7c745a2 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -182,6 +182,7 @@
import com.android.launcher3.util.coroutines.DispatcherProvider;
import com.android.quickstep.BaseContainerInterface;
import com.android.quickstep.GestureState;
+import com.android.quickstep.HighResLoadingState;
import com.android.quickstep.OverviewCommandHelper;
import com.android.quickstep.RecentsAnimationController;
import com.android.quickstep.RecentsAnimationTargets;
@@ -194,7 +195,6 @@
import com.android.quickstep.SplitSelectionListener;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskOverlayFactory;
-import com.android.quickstep.TaskThumbnailCache;
import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.ViewUtils;
@@ -263,14 +263,14 @@
public abstract class RecentsView<
CONTAINER_TYPE extends Context & RecentsViewContainer & StatefulContainer<STATE_TYPE>,
STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
- TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
+ HighResLoadingState.HighResLoadingStateChangedCallback,
TaskVisualsChangeListener {
private static final String TAG = "RecentsView";
private static final boolean DEBUG = false;
- public static final FloatProperty<RecentsView> CONTENT_ALPHA =
- new FloatProperty<RecentsView>("contentAlpha") {
+ public static final FloatProperty<RecentsView<?, ?>> CONTENT_ALPHA =
+ new FloatProperty<>("contentAlpha") {
@Override
public void setValue(RecentsView view, float v) {
view.setContentAlpha(v);
@@ -282,8 +282,8 @@
}
};
- public static final FloatProperty<RecentsView> FULLSCREEN_PROGRESS =
- new FloatProperty<RecentsView>("fullscreenProgress") {
+ public static final FloatProperty<RecentsView<?, ?>> FULLSCREEN_PROGRESS =
+ new FloatProperty<>("fullscreenProgress") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setFullscreenProgress(v);
@@ -295,8 +295,8 @@
}
};
- public static final FloatProperty<RecentsView> TASK_MODALNESS =
- new FloatProperty<RecentsView>("taskModalness") {
+ public static final FloatProperty<RecentsView<?, ?>> TASK_MODALNESS =
+ new FloatProperty<>("taskModalness") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setTaskModalness(v);
@@ -308,8 +308,8 @@
}
};
- public static final FloatProperty<RecentsView> ADJACENT_PAGE_HORIZONTAL_OFFSET =
- new FloatProperty<RecentsView>("adjacentPageHorizontalOffset") {
+ public static final FloatProperty<RecentsView<?, ?>> ADJACENT_PAGE_HORIZONTAL_OFFSET =
+ new FloatProperty<>("adjacentPageHorizontalOffset") {
@Override
public void setValue(RecentsView recentsView, float v) {
if (recentsView.mAdjacentPageHorizontalOffset != v) {
@@ -324,8 +324,8 @@
}
};
- public static final FloatProperty<RecentsView> RUNNING_TASK_ATTACH_ALPHA =
- new FloatProperty<RecentsView>("runningTaskAttachAlpha") {
+ public static final FloatProperty<RecentsView<?, ?>> RUNNING_TASK_ATTACH_ALPHA =
+ new FloatProperty<>("runningTaskAttachAlpha") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.mRunningTaskAttachAlpha = v;
@@ -349,8 +349,8 @@
* Can be used to tint the color of the RecentsView to simulate a scrim that can views
* excluded from. Really should be a proper scrim.
*/
- private static final FloatProperty<RecentsView> COLOR_TINT =
- new FloatProperty<RecentsView>("colorTint") {
+ private static final FloatProperty<RecentsView<?, ?>> COLOR_TINT =
+ new FloatProperty<>("colorTint") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setColorTint(v);
@@ -368,8 +368,8 @@
* more specific, we'd want to create a similar FloatProperty just for a TaskView's
* offsetX/Y property
*/
- public static final FloatProperty<RecentsView> TASK_SECONDARY_TRANSLATION =
- new FloatProperty<RecentsView>("taskSecondaryTranslation") {
+ public static final FloatProperty<RecentsView<?, ?>> TASK_SECONDARY_TRANSLATION =
+ new FloatProperty<>("taskSecondaryTranslation") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setTaskViewsResistanceTranslation(v);
@@ -387,8 +387,8 @@
* more specific, we'd want to create a similar FloatProperty just for a TaskView's
* offsetX/Y property
*/
- public static final FloatProperty<RecentsView> TASK_PRIMARY_SPLIT_TRANSLATION =
- new FloatProperty<RecentsView>("taskPrimarySplitTranslation") {
+ public static final FloatProperty<RecentsView<?, ?>> TASK_PRIMARY_SPLIT_TRANSLATION =
+ new FloatProperty<>("taskPrimarySplitTranslation") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setTaskViewsPrimarySplitTranslation(v);
@@ -400,8 +400,8 @@
}
};
- public static final FloatProperty<RecentsView> TASK_SECONDARY_SPLIT_TRANSLATION =
- new FloatProperty<RecentsView>("taskSecondarySplitTranslation") {
+ public static final FloatProperty<RecentsView<?, ?>> TASK_SECONDARY_SPLIT_TRANSLATION =
+ new FloatProperty<>("taskSecondarySplitTranslation") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setTaskViewsSecondarySplitTranslation(v);
@@ -414,8 +414,8 @@
};
/** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */
- public static final FloatProperty<RecentsView> RECENTS_SCALE_PROPERTY =
- new FloatProperty<RecentsView>("recentsScale") {
+ public static final FloatProperty<RecentsView<?, ?>> RECENTS_SCALE_PROPERTY =
+ new FloatProperty<>("recentsScale") {
@Override
public void setValue(RecentsView view, float scale) {
view.setScaleX(scale);
@@ -444,8 +444,8 @@
* Progress of Recents view from carousel layout to grid layout. If Recents is not shown as a
* grid, then the value remains 0.
*/
- public static final FloatProperty<RecentsView> RECENTS_GRID_PROGRESS =
- new FloatProperty<RecentsView>("recentsGrid") {
+ public static final FloatProperty<RecentsView<?, ?>> RECENTS_GRID_PROGRESS =
+ new FloatProperty<>("recentsGrid") {
@Override
public void setValue(RecentsView view, float gridProgress) {
view.setGridProgress(gridProgress);
@@ -457,7 +457,7 @@
}
};
- public static final FloatProperty<RecentsView> DESKTOP_CAROUSEL_DETACH_PROGRESS =
+ public static final FloatProperty<RecentsView<?, ?>> DESKTOP_CAROUSEL_DETACH_PROGRESS =
new FloatProperty<>("desktopCarouselDetachProgress") {
@Override
public void setValue(RecentsView view, float offset) {
@@ -476,8 +476,8 @@
* Alpha of the task thumbnail splash, where being in BackgroundAppState has a value of 1, and
* being in any other state has a value of 0.
*/
- public static final FloatProperty<RecentsView> TASK_THUMBNAIL_SPLASH_ALPHA =
- new FloatProperty<RecentsView>("taskThumbnailSplashAlpha") {
+ public static final FloatProperty<RecentsView<?, ?>> TASK_THUMBNAIL_SPLASH_ALPHA =
+ new FloatProperty<>("taskThumbnailSplashAlpha") {
@Override
public void setValue(RecentsView view, float taskThumbnailSplashAlpha) {
view.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
@@ -5484,7 +5484,7 @@
firstFloatingTaskView.update(mTempRectF, /*progress=*/1f);
RecentsPagedOrientationHandler orientationHandler = getPagedOrientationHandler();
- Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
+ Pair<FloatProperty<RecentsView<?, ?>>, FloatProperty<RecentsView<?, ?>>> taskViewsFloat =
orientationHandler.getSplitSelectTaskOffset(
TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
mContainer.getDeviceProfile());
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 2f773b3..46b5659 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
@@ -245,9 +245,11 @@
animator.onStashStateChangingWhileAnimating()
}
- // The physics animation test util posts the cancellation to the looper thread, so we have
- // to wait again and let it finish.
- InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // wait for the animation to cancel
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
+ handleAnimator,
+ DynamicAnimation.TRANSLATION_Y,
+ )
// verify that the hide animation was canceled
assertThat(animatorScheduler.delayedBlock).isNull()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
index 7d09efd..4adf01e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
@@ -16,7 +16,7 @@
package com.android.quickstep.recents.data
-import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
class FakeHighResLoadingStateNotifier : HighResLoadingStateNotifier {
val listeners = mutableListOf<HighResLoadingStateChangedCallback>()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
index 5de876a..f6f158f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
@@ -17,11 +17,11 @@
package com.android.quickstep.recents.data
import android.graphics.drawable.Drawable
-import com.android.launcher3.util.CancellableTask
-import com.android.quickstep.TaskIconCache
+import com.android.quickstep.TaskIconCache.TaskCacheEntry
import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
import com.android.systemui.shared.recents.model.Task
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.yield
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@@ -29,28 +29,30 @@
val taskIdToDrawable: MutableMap<Int, Drawable> =
(0..10).associateWith { mockCopyableDrawable() }.toMutableMap()
-
- val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
- var shouldLoadSynchronously: Boolean = true
+ private val completionPrevented: MutableSet<Int> = mutableSetOf()
/** Retrieves and sets an icon on [task] from [taskIdToDrawable]. */
- override fun getIconInBackground(
- task: Task,
- callback: TaskIconCache.GetTaskIconCallback
- ): CancellableTask<*>? {
- val wrappedCallback = {
- callback.onTaskIconReceived(
- taskIdToDrawable.getValue(task.key.id),
- "content desc ${task.key.id}",
- "title ${task.key.id}"
- )
+ override suspend fun getIcon(task: Task): TaskCacheEntry {
+ while (task.key.id in completionPrevented) {
+ yield()
}
- if (shouldLoadSynchronously) {
- wrappedCallback()
- } else {
- taskIdToUpdatingTask[task.key.id] = wrappedCallback
- }
- return null
+ return TaskCacheEntry(
+ taskIdToDrawable.getValue(task.key.id),
+ "content desc ${task.key.id}",
+ "title ${task.key.id}",
+ )
+ }
+
+ fun preventIconLoad(taskId: Int) {
+ completionPrevented.add(taskId)
+ }
+
+ fun completeLoadingForTask(taskId: Int) {
+ completionPrevented.remove(taskId)
+ }
+
+ fun completeLoading() {
+ completionPrevented.clear()
}
companion object {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
index d12c0b0..e10afc4 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
@@ -17,11 +17,10 @@
package com.android.quickstep.recents.data
import android.graphics.Bitmap
-import com.android.launcher3.util.CancellableTask
import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
-import java.util.function.Consumer
+import kotlinx.coroutines.yield
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@@ -29,25 +28,28 @@
val taskIdToBitmap: MutableMap<Int, Bitmap> =
(0..10).associateWith { mock<Bitmap>() }.toMutableMap()
- val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
- var shouldLoadSynchronously: Boolean = true
+ private val completionPrevented: MutableSet<Int> = mutableSetOf()
+ private val getThumbnailCalls = mutableMapOf<Int, Int>()
/** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */
- override fun getThumbnailInBackground(
- task: Task,
- callback: Consumer<ThumbnailData>
- ): CancellableTask<ThumbnailData>? {
- val thumbnailData = mock<ThumbnailData>()
- whenever(thumbnailData.thumbnail).thenReturn(taskIdToBitmap[task.key.id])
- val wrappedCallback = {
- task.thumbnail = thumbnailData
- callback.accept(thumbnailData)
+ override suspend fun getThumbnail(task: Task): ThumbnailData {
+ getThumbnailCalls[task.key.id] = (getThumbnailCalls[task.key.id] ?: 0) + 1
+
+ while (task.key.id in completionPrevented) {
+ yield()
}
- if (shouldLoadSynchronously) {
- wrappedCallback()
- } else {
- taskIdToUpdatingTask[task.key.id] = wrappedCallback
+ return mock<ThumbnailData>().also {
+ whenever(it.thumbnail).thenReturn(taskIdToBitmap[task.key.id])
}
- return null
+ }
+
+ fun getNumberOfGetThumbnailCalls(taskId: Int): Int = getThumbnailCalls[taskId] ?: 0
+
+ fun preventThumbnailLoad(taskId: Int) {
+ completionPrevented.add(taskId)
+ }
+
+ fun completeLoadingForTask(taskId: Int) {
+ completionPrevented.remove(taskId)
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
index ee1ec6e..b6cf5bd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
@@ -214,7 +214,7 @@
.isEqualTo(bitmap2)
// Prevent new loading of Bitmaps
- taskThumbnailDataSource.shouldLoadSynchronously = false
+ taskThumbnailDataSource.preventThumbnailLoad(2)
systemUnderTest.setVisibleTasks(setOf(2, 3))
assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
@@ -235,7 +235,7 @@
.assertHasIconDataFromSource(taskIconDataSource)
// Prevent new loading of Drawables
- taskThumbnailDataSource.shouldLoadSynchronously = false
+ taskIconDataSource.preventIconLoad(2)
systemUnderTest.setVisibleTasks(setOf(2, 3))
systemUnderTest
@@ -257,9 +257,6 @@
assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2)
task2.assertHasIconDataFromSource(taskIconDataSource)
- // Prevent new loading of Bitmaps
- taskThumbnailDataSource.shouldLoadSynchronously = false
- taskIconDataSource.shouldLoadSynchronously = false
systemUnderTest.setVisibleTasks(setOf(0, 1))
val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!!
@@ -275,21 +272,22 @@
// Setup fakes
recentsModel.seedTasks(defaultTaskList)
val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
- taskThumbnailDataSource.shouldLoadSynchronously = false
// Setup TasksRepository
systemUnderTest.getAllTaskData(forceRefresh = true)
- systemUnderTest.setVisibleTasks(setOf(1, 2))
- // Assert there is no bitmap in first emission
- assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull()
+ val task2DataFlow = systemUnderTest.getTaskDataById(2)
+ val task2BitmapValues = mutableListOf<Bitmap?>()
+ testScope.backgroundScope.launch {
+ task2DataFlow.map { it?.thumbnail?.thumbnail }.toList(task2BitmapValues)
+ }
- // Simulate bitmap loading after first emission
- taskThumbnailDataSource.taskIdToUpdatingTask.getValue(2).invoke()
+ // Check for first emission
+ assertThat(task2BitmapValues.single()).isNull()
+ systemUnderTest.setVisibleTasks(setOf(2))
// Check for second emission
- assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
- .isEqualTo(bitmap2)
+ assertThat(task2BitmapValues).isEqualTo(listOf(null, bitmap2))
}
@Test
@@ -365,7 +363,6 @@
testScope.runTest {
recentsModel.seedTasks(defaultTaskList)
systemUnderTest.getAllTaskData(forceRefresh = true)
- taskThumbnailDataSource.shouldLoadSynchronously = false
val taskDataFlow = systemUnderTest.getTaskDataById(1)
val task1IconValues = mutableListOf<Drawable?>()
@@ -374,14 +371,10 @@
}
systemUnderTest.setVisibleTasks(setOf(1))
- val task1UpdatingTaskOld = taskThumbnailDataSource.taskIdToUpdatingTask[1]
- println(task1UpdatingTaskOld)
+ assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(1)).isEqualTo(1)
systemUnderTest.setVisibleTasks(setOf(1, 2))
- val task1UpdatingTaskNew = taskThumbnailDataSource.taskIdToUpdatingTask[1]
- println(task1UpdatingTaskNew)
-
- assertThat(task1UpdatingTaskNew).isEqualTo(task1UpdatingTaskOld)
+ assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(1)).isEqualTo(1)
}
private fun createTaskWithId(taskId: Int) =
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
index 2e91f5c..95504af 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
@@ -22,6 +22,7 @@
import android.graphics.Color
import android.graphics.Matrix
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
@@ -33,7 +34,10 @@
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,6 +45,7 @@
import org.mockito.kotlin.whenever
/** Test for [TaskOverlayViewModel] */
+@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class TaskOverlayViewModelTest {
private val task =
@@ -58,104 +63,123 @@
private val recentsViewData = RecentsViewData()
private val tasksRepository = FakeTasksRepository()
private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
+ private val dispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(dispatcher)
private val systemUnderTest =
- TaskOverlayViewModel(task, recentsViewData, mGetThumbnailPositionUseCase, tasksRepository)
+ TaskOverlayViewModel(
+ task,
+ recentsViewData,
+ mGetThumbnailPositionUseCase,
+ tasksRepository,
+ TestDispatcherProvider(dispatcher),
+ )
@Test
- fun initialStateIsDisabled() = runTest {
- assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
- }
+ fun initialStateIsDisabled() =
+ testScope.runTest { assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled) }
@Test
- fun recentsViewOverlayDisabled_Disabled() = runTest {
- recentsViewData.overlayEnabled.value = false
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ fun recentsViewOverlayDisabled_Disabled() =
+ testScope.runTest {
+ recentsViewData.overlayEnabled.value = false
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
- }
+ assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+ }
@Test
- fun taskNotFullyVisible_Disabled() = runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf()
+ fun taskNotFullyVisible_Disabled() =
+ testScope.runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf()
- assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
- }
+ assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+ }
@Test
- fun noThumbnail_Enabled() = runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- task.isLocked = false
+ fun noThumbnail_Enabled() =
+ testScope.runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ task.isLocked = false
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = null))
- }
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = null))
+ }
@Test
- fun withThumbnail_RealSnapshot_NotLocked_Enabled() = runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
- thumbnailData.isRealSnapshot = true
- task.isLocked = false
+ fun withThumbnail_RealSnapshot_NotLocked_Enabled() =
+ testScope.runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+ tasksRepository.setVisibleTasks(setOf(TASK_ID))
+ thumbnailData.isRealSnapshot = true
+ task.isLocked = false
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = true, thumbnail = thumbnailData.thumbnail))
- }
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(Enabled(isRealSnapshot = true, thumbnail = thumbnailData.thumbnail))
+ }
@Test
- fun withThumbnail_RealSnapshot_Locked_Enabled() = runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
- thumbnailData.isRealSnapshot = true
- task.isLocked = true
+ fun withThumbnail_RealSnapshot_Locked_Enabled() =
+ testScope.runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+ tasksRepository.setVisibleTasks(setOf(TASK_ID))
+ thumbnailData.isRealSnapshot = true
+ task.isLocked = true
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
- }
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
+ }
@Test
- fun withThumbnail_FakeSnapshot_Enabled() = runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
- thumbnailData.isRealSnapshot = false
- task.isLocked = false
+ fun withThumbnail_FakeSnapshot_Enabled() =
+ testScope.runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+ tasksRepository.setVisibleTasks(setOf(TASK_ID))
+ thumbnailData.isRealSnapshot = false
+ task.isLocked = false
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
- }
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
+ }
@Test
- fun getThumbnailMatrix_MissingThumbnail() = runTest {
- val isRtl = true
+ fun getThumbnailMatrix_MissingThumbnail() =
+ testScope.runTest {
+ val isRtl = true
- whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MissingThumbnail)
+ whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .thenReturn(MissingThumbnail)
- assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .isEqualTo(ThumbnailPositionState(Matrix.IDENTITY_MATRIX, isRotated = false))
- }
+ assertThat(
+ systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+ )
+ .isEqualTo(ThumbnailPositionState(Matrix.IDENTITY_MATRIX, isRotated = false))
+ }
@Test
- fun getThumbnailMatrix_MatrixScaling() = runTest {
- val isRtl = true
- val isRotated = true
+ fun getThumbnailMatrix_MatrixScaling() =
+ testScope.runTest {
+ val isRtl = true
+ val isRotated = true
- whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MatrixScaling(MATRIX, isRotated))
+ whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .thenReturn(MatrixScaling(MATRIX, isRotated))
- assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .isEqualTo(ThumbnailPositionState(MATRIX, isRotated))
- }
+ assertThat(
+ systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+ )
+ .isEqualTo(ThumbnailPositionState(MATRIX, isRotated))
+ }
companion object {
const val TASK_ID = 0
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
index 648fa93..ef4591e 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
@@ -44,7 +44,6 @@
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.TaskStackChangeListeners;
-import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -67,7 +66,7 @@
private RecentTasksList mTasksList;
@Mock
- private TaskThumbnailCache.HighResLoadingState mHighResLoadingState;
+ private HighResLoadingState mHighResLoadingState;
private RecentsModel mRecentsModel;
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index ec6a9c3..961dc58 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -337,7 +337,6 @@
@Test
@NavigationModeSwitch
@PortraitLandscape
- @ScreenRecord // b/313464374
@TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406
public void testQuickSwitchFromApp() throws Exception {
startTestActivity(2);
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6f293b6..501e650 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -308,6 +308,8 @@
<string name="folder_name_format_exact">Folder: <xliff:g id="name" example="Games">%1$s</xliff:g>, <xliff:g id="size" example="2">%2$d</xliff:g> items</string>
<!-- Folder name format when folder has 4 or more items shown in preview-->
<string name="folder_name_format_overflow">Folder: <xliff:g id="name" example="Games">%1$s</xliff:g>, <xliff:g id="size" example="2">%2$d</xliff:g> or more items</string>
+ <!-- Accessibility announement for unnamed folders -->
+ <string name="unnamed_folder">Unnamed folder</string>
<!-- App pair accessibility -->
<!-- App pair name -->
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 61297ce..da73280 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -367,27 +367,9 @@
@UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
- applyFromWorkspaceItem(info, /* animate = */ false, /* staggerIndex = */ 0);
- }
-
- @UiThread
- public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
applyFromWorkspaceItem(info, null);
}
- /**
- * Returns whether the newInfo differs from the current getTag().
- */
- public boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) {
- WorkspaceItemInfo oldInfo = getTag() instanceof WorkspaceItemInfo
- ? (WorkspaceItemInfo) getTag()
- : null;
- boolean changedIcons = oldInfo != null && oldInfo.getTargetComponent() != null
- && newInfo.getTargetComponent() != null
- && !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent());
- return changedIcons && isShown();
- }
-
@Override
public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
if (delegate instanceof BaseAccessibilityDelegate) {
@@ -1326,7 +1308,6 @@
applyFromApplicationInfo((AppInfo) info);
} else if (info instanceof WorkspaceItemInfo) {
applyFromWorkspaceItem((WorkspaceItemInfo) info);
- mActivity.invalidateParent(info);
} else if (info != null) {
applyFromItemInfoWithIcon(info);
}
@@ -1340,12 +1321,11 @@
* Verifies that the current icon is high-res otherwise posts a request to load the icon.
*/
public void verifyHighRes() {
- if (mIconLoadRequest != null) {
- mIconLoadRequest.cancel();
- mIconLoadRequest = null;
- }
if (getTag() instanceof ItemInfoWithIcon info && !mHighResUpdateInProgress
&& info.getMatchingLookupFlag().useLowRes()) {
+ if (mIconLoadRequest != null) {
+ mIconLoadRequest.cancel();
+ }
mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
.updateIconInBackground(BubbleTextView.this, info);
}
diff --git a/src/com/android/launcher3/DropTargetHandler.kt b/src/com/android/launcher3/DropTargetHandler.kt
index 4d3fe52..66c948a 100644
--- a/src/com/android/launcher3/DropTargetHandler.kt
+++ b/src/com/android/launcher3/DropTargetHandler.kt
@@ -66,6 +66,11 @@
fun onDeleteComplete(item: ItemInfo) {
removeItemAndStripEmptyScreens(null /* view */, item)
+ AbstractFloatingView.closeOpenViews(
+ mLauncher,
+ false,
+ AbstractFloatingView.TYPE_WIDGET_RESIZE_FRAME,
+ )
var pageItem: ItemInfo = item
if (item.container <= 0) {
val v = mLauncher.workspace.getHomescreenIconByItemId(item.container)
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 3fbf888..4097dca 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -72,7 +72,6 @@
import static com.android.launcher3.Utilities.postAsyncCallback;
import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
-import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.SHOW;
import static com.android.launcher3.logging.StatsLogManager.EventEnum;
@@ -206,7 +205,6 @@
import com.android.launcher3.model.ModelWriter;
import com.android.launcher3.model.StringCache;
import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.AppPairInfo;
import com.android.launcher3.model.data.CollectionInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
@@ -829,26 +827,6 @@
return true;
}
- @Override
- public void invalidateParent(ItemInfo info) {
- if (info.container >= 0) {
- View collectionIcon = getWorkspace().getHomescreenIconByItemId(info.container);
- if (collectionIcon instanceof FolderIcon folderIcon
- && collectionIcon.getTag() instanceof FolderInfo) {
- if (createFolderGridOrganizer(getDeviceProfile())
- .setFolderInfo((FolderInfo) folderIcon.getTag())
- .isItemInPreview(info.rank)) {
- folderIcon.invalidate();
- }
- } else if (collectionIcon instanceof AppPairIcon appPairIcon
- && collectionIcon.getTag() instanceof AppPairInfo appPairInfo) {
- if (appPairInfo.getContents().contains(info)) {
- appPairIcon.getIconDrawableArea().redraw();
- }
- }
- }
- }
-
/**
* Returns whether we should delay spring loaded mode -- for shortcuts and widgets that have
* a configuration step, this allows the proper animations to run after other transitions.
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 81d6631..78b53a9 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -199,6 +199,8 @@
host.requestFocus();
host.sendAccessibilityEvent(TYPE_VIEW_FOCUSED);
host.performAccessibilityAction(ACTION_ACCESSIBILITY_FOCUS, null);
+ AbstractFloatingView.closeOpenViews(mContext, /* animate= */ false,
+ AbstractFloatingView.TYPE_WIDGET_RESIZE_FRAME);
});
return true;
} else if (action == DEEP_SHORTCUTS) {
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index de1bcc3..2481a1a 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -816,6 +816,10 @@
* Returns a formatted accessibility title for folder
*/
public String getAccessiblityTitle(CharSequence title) {
+ if (title == null) {
+ // Avoids "Talkback -> Folder: null" announcement.
+ title = getContext().getString(R.string.unnamed_folder);
+ }
int size = mInfo.getContents().size();
if (size < MAX_NUM_ITEMS_IN_PREVIEW) {
return getContext().getString(R.string.folder_name_format_exact, title, size);
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 8d751e6..22f1164 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -24,7 +24,6 @@
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Path;
-import android.graphics.drawable.Drawable;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.Log;
@@ -541,19 +540,10 @@
ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets();
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
View iconView = parent.getChildAt(i);
- Drawable d = null;
if (iconView instanceof BubbleTextView btv) {
btv.verifyHighRes();
- d = btv.getIcon();
} else if (iconView instanceof AppPairIcon api) {
api.verifyHighRes();
- d = api.getIconDrawableArea().getDrawable();
- }
-
- // Set the callback back to the actual icon, in case
- // it was captured by the FolderIcon
- if (d != null) {
- d.setCallback(iconView);
}
}
}
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 2276ac7..5ee6a25 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -17,6 +17,7 @@
package com.android.launcher3.folder;
import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER;
+import static com.android.launcher3.LauncherSettings.Favorites.DESKTOP_ICON_FLAG;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ENTER_INDEX;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDEX;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
@@ -43,12 +44,14 @@
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.Flags;
+import com.android.launcher3.LauncherAppState;
import com.android.launcher3.Utilities;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.apppairs.AppPairIconDrawingParams;
import com.android.launcher3.apppairs.AppPairIconGraphic;
import com.android.launcher3.model.data.AppPairInfo;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.ActivityContext;
@@ -460,6 +463,18 @@
// Set the callback to FolderIcon as it is responsible to drawing the icon. The
// callback will be released when the folder is opened.
p.drawable.setCallback(mIcon);
+
+ // Verify high res
+ if (item instanceof ItemInfoWithIcon info
+ && info.getMatchingLookupFlag().isVisuallyLessThan(DESKTOP_ICON_FLAG)) {
+ LauncherAppState.getInstance(mContext).getIconCache().updateIconInBackground(
+ newInfo -> {
+ if (p.item == newInfo) {
+ setDrawable(p, newInfo);
+ mIcon.invalidate();
+ }
+ }, info);
+ }
}
/**
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index b8481c5..b164b7f 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -105,13 +105,6 @@
return null;
}
- /**
- * For items with tree hierarchy, notifies the activity to invalidate the parent when a root
- * is invalidated
- * @param info info associated with a root node.
- */
- default void invalidateParent(ItemInfo info) { }
-
default AccessibilityDelegate getAccessibilityDelegate() {
return null;
}
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
index b877d7a..94ae69c 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
@@ -1,5 +1,7 @@
package com.android.launcher3.widget;
+import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE;
+
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
@@ -14,6 +16,7 @@
import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.icons.cache.BaseIconCache;
@@ -108,6 +111,13 @@
Point cellSize = new Point();
for (DeviceProfile dp : idp.supportedProfiles) {
+ // On phones we no longer support regular landscape, only fixed landscape for this
+ // reason we don't need to take regular landscape into account in phones
+ if (Flags.oneGridSpecs() && dp.inv.deviceType == TYPE_PHONE
+ && dp.inv.isFixedLandscape != dp.isLandscape) {
+ continue;
+ }
+
dp.getCellSize(cellSize);
Rect widgetPadding = dp.widgetPadding;
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt
index 8952b85..2e556e8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt
@@ -80,6 +80,15 @@
)
}
+ /**
+ * Sets the test app for app icons to the specified Component
+ *
+ * @param testAppComponent ComponentName to use for app icons
+ */
+ fun setTestAppComponent(testAppComponent: ComponentName) {
+ appComponentName = testAppComponent
+ }
+
private fun addCorrespondingWidgetRect(
widgetRect: WidgetRect,
transaction: FavoriteItemsTransaction,
diff --git a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
index 7f481b7..548cf5b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
@@ -17,21 +17,22 @@
package com.android.launcher3.folder
import android.R
-import android.content.Context
import android.graphics.Bitmap
import android.os.Process
-import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherPrefs.Companion.THEMED_ICONS
import com.android.launcher3.LauncherPrefs.Companion.get
import com.android.launcher3.graphics.PreloadIconDrawable
+import com.android.launcher3.icons.BitmapInfo
import com.android.launcher3.icons.FastBitmapDrawable
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver
+import com.android.launcher3.icons.PlaceHolderIconDrawable
import com.android.launcher3.icons.UserBadgeDrawable
import com.android.launcher3.icons.mono.MonoThemedBitmap
import com.android.launcher3.model.data.FolderInfo
-import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED
import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE
import com.android.launcher3.model.data.WorkspaceItemInfo
@@ -40,6 +41,7 @@
import com.android.launcher3.util.FlagOp
import com.android.launcher3.util.LauncherLayoutBuilder
import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
import com.android.launcher3.util.TestUtil
import com.android.launcher3.util.UserIconInfo
import com.google.common.truth.Truth.assertThat
@@ -47,6 +49,13 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
/** Tests for [PreviewItemManager] */
@SmallTest
@@ -54,22 +63,27 @@
class PreviewItemManagerTest {
private lateinit var previewItemManager: PreviewItemManager
- private lateinit var context: Context
- private lateinit var folderItems: ArrayList<ItemInfo>
+ private lateinit var context: SandboxModelContext
+ private lateinit var folderItems: ArrayList<WorkspaceItemInfo>
private lateinit var modelHelper: LauncherModelHelper
private lateinit var folderIcon: FolderIcon
+ private lateinit var iconCache: IconCache
private var defaultThemedIcons = false
@Before
fun setup() {
- getInstrumentation().runOnMainSync {
- folderIcon =
- FolderIcon(ActivityContextWrapper(ApplicationProvider.getApplicationContext()))
- }
- context = getInstrumentation().targetContext
- previewItemManager = PreviewItemManager(folderIcon)
modelHelper = LauncherModelHelper()
+ context = modelHelper.sandboxContext
+ folderIcon = FolderIcon(ActivityContextWrapper(context))
+
+ val app = spy(LauncherAppState.getInstance(context))
+ iconCache = spy(app.iconCache)
+ doReturn(iconCache).whenever(app).iconCache
+ context.putObject(LauncherAppState.INSTANCE, app)
+ doReturn(null).whenever(iconCache).updateIconInBackground(any(), any())
+
+ previewItemManager = PreviewItemManager(folderIcon)
modelHelper
.setupDefaultLayoutProvider(
LauncherLayoutBuilder()
@@ -82,33 +96,35 @@
.build()
)
.loadModelSync()
- folderItems = modelHelper.bgDataModel.collections.valueAt(0).getContents()
+
+ // Use getAppContents() to "cast" contents to WorkspaceItemInfo so we can set bitmaps
+ folderItems = modelHelper.bgDataModel.collections.valueAt(0).getAppContents()
folderIcon.mInfo = modelHelper.bgDataModel.collections.valueAt(0) as FolderInfo
folderIcon.mInfo.getContents().addAll(folderItems)
- // Use getAppContents() to "cast" contents to WorkspaceItemInfo so we can set bitmaps
- val folderApps = modelHelper.bgDataModel.collections.valueAt(0).getAppContents()
// Set first icon to be themed.
- folderApps[0].bitmap.themedBitmap =
+ folderItems[0].bitmap.themedBitmap =
MonoThemedBitmap(
- folderApps[0].bitmap.icon,
+ folderItems[0].bitmap.icon,
Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888),
)
// Set second icon to be non-themed.
- folderApps[1].bitmap.themedBitmap = null
+ folderItems[1].bitmap.themedBitmap = null
// Set third icon to be themed with badge.
- folderApps[2].bitmap.themedBitmap =
+ folderItems[2].bitmap.themedBitmap =
MonoThemedBitmap(
- folderApps[2].bitmap.icon,
+ folderItems[2].bitmap.icon,
Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888),
)
- folderApps[2].bitmap = folderApps[2].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
+ folderItems[2].bitmap =
+ folderItems[2].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
// Set fourth icon to be non-themed with badge.
- folderApps[3].bitmap = folderApps[3].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
- folderApps[3].bitmap.themedBitmap = null
+ folderItems[3].bitmap =
+ folderItems[3].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
+ folderItems[3].bitmap.themedBitmap = null
defaultThemedIcons = get(context).get(THEMED_ICONS)
}
@@ -232,6 +248,31 @@
assertThat(drawingParams.drawable).isInstanceOf(PreloadIconDrawable::class.java)
}
+ @Test
+ fun `Preview item loads and apply high res icon`() {
+ val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
+ val originalBitmap = folderItems[3].bitmap
+ folderItems[3].bitmap = BitmapInfo.LOW_RES_INFO
+
+ previewItemManager.setDrawable(drawingParams, folderItems[3])
+ assertThat(drawingParams.drawable).isInstanceOf(PlaceHolderIconDrawable::class.java)
+
+ val callbackCaptor = argumentCaptor<ItemInfoUpdateReceiver>()
+ verify(iconCache).updateIconInBackground(callbackCaptor.capture(), eq(folderItems[3]))
+
+ // Restore high-res icon
+ folderItems[3].bitmap = originalBitmap
+
+ // Calling with a different item info will ignore the update
+ callbackCaptor.firstValue.reapplyItemInfo(folderItems[2])
+ assertThat(drawingParams.drawable).isInstanceOf(PlaceHolderIconDrawable::class.java)
+
+ // Calling with correct value will update the drawable to high-res
+ callbackCaptor.firstValue.reapplyItemInfo(folderItems[3])
+ assertThat(drawingParams.drawable).isNotInstanceOf(PlaceHolderIconDrawable::class.java)
+ assertThat(drawingParams.drawable).isInstanceOf(FastBitmapDrawable::class.java)
+ }
+
private fun profileFlagOp(type: Int) =
UserIconInfo(Process.myUserHandle(), type).applyBitmapInfoFlags(FlagOp.NO_OP)
}