Merge "Update Overview flag bug IDs" into main
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 6196be4..36e6902 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -371,7 +371,6 @@
<dimen name="taskbar_running_app_indicator_height">2dp</dimen>
<dimen name="taskbar_running_app_indicator_width">12dp</dimen>
<dimen name="taskbar_running_app_indicator_top_margin">4dp</dimen>
- <dimen name="taskbar_minimized_app_indicator_width">6dp</dimen>
<!-- Transient taskbar -->
<dimen name="transient_taskbar_padding">12dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/taskbar/PinToTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/PinToTaskbarShortcut.kt
index b9a211d..da6932f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/PinToTaskbarShortcut.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/PinToTaskbarShortcut.kt
@@ -17,9 +17,15 @@
package com.android.launcher3.taskbar
import android.content.Context
+import android.util.SparseArray
import android.view.View
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
import com.android.launcher3.R
+import com.android.launcher3.model.BgDataModel
import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.popup.SystemShortcut
import com.android.launcher3.views.ActivityContext
@@ -27,16 +33,59 @@
* A single menu item shortcut to allow users to pin an item to the taskbar and unpin an item from
* the taskbar.
*/
-class PinToTaskbarShortcut<T>(target: T, itemInfo: ItemInfo?, originalView: View, isPin: Boolean) :
+class PinToTaskbarShortcut<T>(
+ target: T,
+ itemInfo: ItemInfo?,
+ originalView: View,
+ private val mIsPin: Boolean,
+ private val mPinnedInfoList: SparseArray<ItemInfo?>,
+) :
SystemShortcut<T>(
- if (isPin) R.drawable.ic_pin else R.drawable.ic_unpin,
- if (isPin) R.string.pin_to_taskbar else R.string.unpin_from_taskbar,
+ if (mIsPin) R.drawable.ic_pin else R.drawable.ic_unpin,
+ if (mIsPin) R.string.pin_to_taskbar else R.string.unpin_from_taskbar,
target,
itemInfo,
originalView,
) where T : Context?, T : ActivityContext? {
override fun onClick(v: View?) {
- // TODO(b/375648361): Pin/Unpin the item here.
+ dismissTaskMenuView()
+ // Create a placeholder callbacks for the writer to notify other launcher model callbacks
+ // after update.
+ val callbacks: BgDataModel.Callbacks = object : BgDataModel.Callbacks {}
+
+ val writer =
+ LauncherAppState.getInstance(mOriginalView.context)
+ .model
+ .getWriter(true, mTarget!!.cellPosMapper, callbacks)
+
+ if (!mIsPin) {
+ writer.deleteItemFromDatabase(mItemInfo, "item unpinned through long-press menu")
+ return
+ }
+
+ val newInfo =
+ if (mItemInfo is com.android.launcher3.model.data.AppInfo) {
+ mItemInfo.makeWorkspaceItem(mOriginalView.context)
+ } else if (mItemInfo is WorkspaceItemInfo) {
+ mItemInfo.clone()
+ } else {
+ return
+ }
+
+ val dp: DeviceProfile = mTarget.deviceProfile
+ var targetIdx = -1
+
+ for (i in 0 until dp.numShownHotseatIcons) {
+ if (mPinnedInfoList[i] == null) {
+ targetIdx = i
+ break
+ }
+ }
+
+ val cellX = if (dp.isVerticalBarLayout()) 0 else targetIdx
+ val cellY = if (dp.isVerticalBarLayout()) (dp.numShownHotseatIcons - (targetIdx + 1)) else 0
+
+ writer.addItemToDatabase(newInfo, CONTAINER_HOTSEAT, mItemInfo.screenId, cellX, cellY)
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
index 017a12c..d909d19 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
@@ -165,7 +165,6 @@
private boolean mIsRtlLayout;
private final List<Task> mItems = new ArrayList<Task>();
private int mIconSize;
- private int mPadding;
private Paint mItemBackgroundPaint;
private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
private float mScaleForReorderBounce = 1f;
@@ -214,25 +213,25 @@
TaskbarOverflowView icon = (TaskbarOverflowView) inflater.inflate(resId, group, false);
icon.mIconSize = iconSize;
- icon.mPadding = padding;
final float taskbarIconRadius =
- iconSize * IconNormalizer.ICON_VISIBLE_AREA_FACTOR / 2f - padding;
+ (iconSize - padding * 2f) * IconNormalizer.ICON_VISIBLE_AREA_FACTOR / 2f;
icon.mLeaveBehindSizeDefault = taskbarIconRadius; // 1/2 of taskbar app icon size
icon.mLeaveBehindSizeScaledDown =
icon.mLeaveBehindSizeDefault * LEAVE_BEHIND_SIZE_SCALE_DOWN_MULTIPLIER;
icon.mLeaveBehindSize = icon.mLeaveBehindSizeScaledDown;
- icon.mItemIconStrokeWidthDefault = taskbarIconRadius / 5f; // 1/10 of taskbar app icon size
+ icon.mItemIconStrokeWidthDefault =
+ taskbarIconRadius / 10f; // 1/20 of taskbar app icon size
icon.mItemIconStrokeWidth = icon.mItemIconStrokeWidthDefault;
- icon.mItemIconSizeDefault = 2 * (taskbarIconRadius - icon.mItemIconStrokeWidthDefault)
- * TWO_ITEM_ICONS_BOX_ASPECT_RATIO;
+ icon.mItemIconSizeDefault = 2f * taskbarIconRadius * TWO_ITEM_ICONS_BOX_ASPECT_RATIO;
icon.mItemIconSizeScaledDown = icon.mLeaveBehindSizeScaledDown;
icon.mItemIconSize = icon.mItemIconSizeDefault;
- icon.mItemIconCenterOffsetDefault = taskbarIconRadius - icon.mItemIconSizeDefault / 2f
+ icon.mItemIconCenterOffsetDefault = taskbarIconRadius
+ - icon.mItemIconSizeDefault * IconNormalizer.ICON_VISIBLE_AREA_FACTOR / 2f
- icon.mItemIconStrokeWidthDefault;
icon.mItemIconCenterOffset = icon.mItemIconCenterOffsetDefault;
@@ -242,9 +241,8 @@
private void init() {
mIsRtlLayout = Utilities.isRtl(getResources());
mItemBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mItemBackgroundColor = getContext().getColor(Utilities.isDarkTheme(getContext())
- ? com.android.internal.R.color.materialColorSurface
- : com.android.internal.R.color.materialColorInverseOnSurface);
+ mItemBackgroundColor = getContext().getColor(
+ com.android.internal.R.color.materialColorInverseOnSurface);
mLeaveBehindColor = Themes.getAttrColor(getContext(), android.R.attr.textColorTertiary);
setWillNotDraw(false);
@@ -260,8 +258,9 @@
private void drawAppIcons(@NonNull Canvas canvas) {
mItemBackgroundPaint.setColor(mItemBackgroundColor);
- float radius = mIconSize / 2f - mPadding;
+ float canvasCenterXY = mIconSize / 2f;
int adjustedItemIconSize = Math.round(mItemIconSize);
+ float itemIconRadius = adjustedItemIconSize / 2f;
int itemsToShow = Math.min(mItems.size(), MAX_ITEMS_IN_PREVIEW);
for (int i = itemsToShow - 1; i >= 0; --i) {
@@ -280,12 +279,12 @@
BlendMode.SRC_ATOP));
canvas.save();
- float itemIconRadius = adjustedItemIconSize / 2f;
canvas.translate(
- mPadding + itemCenterX + radius - itemIconRadius,
- mPadding + itemCenterY + radius - itemIconRadius);
+ canvasCenterXY + itemCenterX - itemIconRadius,
+ canvasCenterXY + itemCenterY - itemIconRadius);
canvas.drawCircle(itemIconRadius, itemIconRadius,
- itemIconRadius + mItemIconStrokeWidth, mItemBackgroundPaint);
+ itemIconRadius * IconNormalizer.ICON_VISIBLE_AREA_FACTOR + mItemIconStrokeWidth,
+ mItemBackgroundPaint);
iconCopy.draw(canvas);
canvas.restore();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index 6ab71e9..1a6cd60 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -216,11 +216,13 @@
return null;
}
if (itemInfo.container == CONTAINER_HOTSEAT) {
- return new PinToTaskbarShortcut<>(target, itemInfo, originalView, false);
+ return new PinToTaskbarShortcut<>(target, itemInfo, originalView, false,
+ mHotseatInfosList);
}
if (mHotseatInfosList.size()
< mContext.getTaskbarSpecsEvaluator().getNumShownHotseatIcons()) {
- return new PinToTaskbarShortcut<>(target, itemInfo, originalView, true);
+ return new PinToTaskbarShortcut<>(target, itemInfo, originalView, true,
+ mHotseatInfosList);
}
return null;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 0fe0224..a80e2c4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -24,6 +24,7 @@
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.app.animation.Interpolators.FINAL_FRAME;
import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.BubbleTextView.LINE_INDICATOR_ANIM_DURATION;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.Flags.taskbarOverflow;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
@@ -138,6 +139,8 @@
private static final int TRANSITION_DEFAULT_DURATION = 500;
private static final int TRANSITION_FADE_IN_DURATION = 167;
private static final int TRANSITION_FADE_OUT_DURATION = 83;
+ private static final int APPEARING_LINE_INDICATOR_ANIM_DELAY =
+ TRANSITION_DEFAULT_DURATION - LINE_INDICATOR_ANIM_DURATION;
private final TaskbarActivityContext mActivity;
private @Nullable TaskbarDragLayerController mDragLayerController;
@@ -736,7 +739,7 @@
public void updateIconViewsRunningStates() {
for (View iconView : getIconViews()) {
if (iconView instanceof BubbleTextView btv) {
- btv.updateRunningState(getRunningAppState(btv));
+ updateRunningState(btv);
if (shouldUpdateIconContentDescription(btv)) {
btv.setContentDescription(
btv.getContentDescription() + " " + btv.getIconStateDescription());
@@ -770,6 +773,10 @@
return pinnedAppsWithTasks;
}
+ private void updateRunningState(BubbleTextView btv) {
+ btv.updateRunningState(getRunningAppState(btv), mTaskbarView.getLayoutTransition() != null);
+ }
+
private BubbleTextView.RunningAppState getRunningAppState(BubbleTextView btv) {
Object tag = btv.getTag();
if (tag instanceof TaskItemInfo itemInfo) {
@@ -1225,13 +1232,22 @@
view.setAlpha(0f);
view.setScaleX(0f);
view.setScaleY(0f);
+ if (view instanceof BubbleTextView btv) {
+ // Defer so that app is mostly scaled in before showing indicator.
+ btv.setLineIndicatorAnimStartDelay(APPEARING_LINE_INDICATOR_ANIM_DELAY);
+ }
+ } else if (type == DISAPPEARING && view instanceof BubbleTextView btv) {
+ // Running state updates happen after removing this view, so update it here.
+ updateRunningState(btv);
}
}
@Override
public void endTransition(
LayoutTransition transition, ViewGroup container, View view, int type) {
- // Do nothing.
+ if (type == APPEARING && view instanceof BubbleTextView btv) {
+ btv.setLineIndicatorAnimStartDelay(0);
+ }
}
});
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 30cfafe..0da8c1f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -215,6 +215,7 @@
animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY, initialVelocity ?: 0f)
animator.addUpdateListener { handle, values ->
val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
+ if (animatingBubble == null) return@addUpdateListener
when {
ty >= stashedHandleTranslationYForAnimation -> {
// we're in the first leg of the animation. only animate the handle. the bubble
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index deb06c9..df66a5e 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -42,8 +42,7 @@
import com.android.launcher3.util.ResourceBasedOverride;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.Snackbar;
-import com.android.quickstep.task.thumbnail.TaskOverlayUiState;
-import com.android.quickstep.task.util.TaskOverlayHelper;
+import com.android.quickstep.recents.domain.usecase.ThumbnailPosition;
import com.android.quickstep.util.RecentsOrientedState;
import com.android.quickstep.views.DesktopTaskView;
import com.android.quickstep.views.GroupedTaskView;
@@ -53,6 +52,7 @@
import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.ThumbnailData;
import java.util.ArrayList;
import java.util.List;
@@ -135,54 +135,37 @@
private T mActionsView;
protected ImageActionsApi mImageApi;
- protected TaskOverlayHelper mHelper;
+ private ThumbnailData mThumbnailData = null;
protected TaskOverlay(TaskContainer taskContainer) {
mApplicationContext = taskContainer.getTaskView().getContext().getApplicationContext();
mTaskContainer = taskContainer;
- if (enableRefactorTaskThumbnail()) {
- mHelper = new TaskOverlayHelper(mTaskContainer.getTask(), this);
- }
mImageApi = new ImageActionsApi(mApplicationContext, this::getThumbnail);
}
- /**
- * Initialize the overlay when a Task is bound to the TaskView.
- */
- public void init() {
- if (enableRefactorTaskThumbnail()) {
- mHelper.init();
- }
- }
-
- /**
- * Destroy the overlay when the TaskView is recycled.
- */
- public void destroy() {
- if (enableRefactorTaskThumbnail()) {
- mHelper.destroy();
- }
+ public void setThumbnailState(@Nullable ThumbnailData thumbnailData) {
+ mThumbnailData = thumbnailData;
}
protected @Nullable Bitmap getThumbnail() {
- return enableRefactorTaskThumbnail() ? mHelper.getEnabledState().getThumbnail()
- : mTaskContainer.getThumbnailViewDeprecated().getThumbnail();
+ if (enableRefactorTaskThumbnail()) {
+ return mThumbnailData == null ? null : mThumbnailData.getThumbnail();
+ } else {
+ return mTaskContainer.getThumbnailViewDeprecated().getThumbnail();
+ }
}
-
/**
* Returns whether the snapshot is real. If the device is locked for the user of the task,
* the snapshot used will be an app-theme generated snapshot instead of a real snapshot.
*/
- public boolean isRealSnapshot() {
+ protected boolean isRealSnapshot() {
if (enableRefactorTaskThumbnail()) {
- if (mHelper.getUiState() instanceof TaskOverlayUiState.Enabled) {
- return mHelper.getEnabledState().isRealSnapshot();
- } else {
- return false;
- }
- }
+ if (mThumbnailData == null) return false;
- return mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot();
+ return mThumbnailData.isRealSnapshot && !mTaskContainer.getTask().isLocked;
+ } else {
+ return mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot();
+ }
}
/**
@@ -190,7 +173,8 @@
*/
public boolean isThumbnailRotationDifferentFromTask() {
if (enableRefactorTaskThumbnail()) {
- return mHelper.getThumbnailPositionState().isRotated();
+ ThumbnailPosition thumbnailPosition = mTaskContainer.getThumbnailPosition();
+ return thumbnailPosition != null && thumbnailPosition.isRotated();
}
return mTaskContainer.getThumbnailViewDeprecated()
@@ -340,9 +324,16 @@
// inverse tells us where the view would be in the bitmaps coordinates. The insets are
// the difference between the bitmap bounds and the projected view bounds.
Matrix boundsToBitmapSpace = new Matrix();
- Matrix thumbnailMatrix = enableRefactorTaskThumbnail()
- ? mHelper.getThumbnailMatrix()
- : mTaskContainer.getThumbnailViewDeprecated().getThumbnailMatrix();
+ Matrix thumbnailMatrix;
+ if (enableRefactorTaskThumbnail()) {
+ if (mTaskContainer.getThumbnailPosition() != null) {
+ thumbnailMatrix = mTaskContainer.getThumbnailPosition().getMatrix();
+ } else {
+ thumbnailMatrix = Matrix.IDENTITY_MATRIX;
+ }
+ } else {
+ thumbnailMatrix = mTaskContainer.getThumbnailViewDeprecated().getThumbnailMatrix();
+ }
thumbnailMatrix.invert(boundsToBitmapSpace);
RectF boundsInBitmapSpace = new RectF();
boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt
index 118a931..8a6a805 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt
@@ -36,6 +36,7 @@
val isLiveTile: Boolean,
val hasHeader: Boolean,
val sysUiStatusNavFlags: Int,
+ val taskOverlayEnabled: Boolean,
)
sealed class TaskData {
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
index 3c4a384..09e2071 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
@@ -29,6 +29,7 @@
import com.android.quickstep.recents.domain.usecase.ThumbnailPosition
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.views.TaskViewType
+import com.android.quickstep.views.TaskViewType.SINGLE
import com.android.systemui.shared.recents.model.ThumbnailData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -74,8 +75,18 @@
)
}
+ private val overlayEnabled =
+ combine(recentsViewData.overlayEnabled, recentsViewData.settledFullyVisibleTaskIds) {
+ isOverlayEnabled,
+ settledFullyVisibleTaskIds ->
+ taskViewType == SINGLE &&
+ isOverlayEnabled &&
+ settledFullyVisibleTaskIds.any { it in taskIds.value }
+ }
+ .distinctUntilChanged()
+
val state: Flow<TaskTileUiState> =
- combine(taskData, isLiveTile) { tasks, isLiveTile -> mapToTaskTile(tasks, isLiveTile) }
+ combine(taskData, isLiveTile, overlayEnabled, ::mapToTaskTile)
.distinctUntilChanged()
.flowOn(dispatcherProvider.background)
@@ -99,13 +110,18 @@
isRtl = isRtl,
)
- private fun mapToTaskTile(tasks: List<TaskData>, isLiveTile: Boolean): TaskTileUiState {
+ private fun mapToTaskTile(
+ tasks: List<TaskData>,
+ isLiveTile: Boolean,
+ overlayEnabled: Boolean,
+ ): TaskTileUiState {
val firstThumbnailData = (tasks.firstOrNull() as? TaskData.Data)?.thumbnailData
return TaskTileUiState(
tasks = tasks,
isLiveTile = isLiveTile,
hasHeader = taskViewType == TaskViewType.DESKTOP,
sysUiStatusNavFlags = getSysUiStatusNavFlagsUseCase(firstThumbnailData),
+ taskOverlayEnabled = overlayEnabled,
)
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
deleted file mode 100644
index 5fb5b90..0000000
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.task.thumbnail
-
-import android.graphics.Bitmap
-
-/** Ui state for [com.android.quickstep.TaskOverlayFactory.TaskOverlay] */
-sealed class TaskOverlayUiState {
- data object Disabled : TaskOverlayUiState()
-
- data class Enabled(val isRealSnapshot: Boolean, val thumbnail: Bitmap?) : TaskOverlayUiState()
-}
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
deleted file mode 100644
index d8aea9d..0000000
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.task.util
-
-import android.util.Log
-import android.view.View.OnLayoutChangeListener
-import com.android.launcher3.util.coroutines.DispatcherProvider
-import com.android.quickstep.TaskOverlayFactory
-import com.android.quickstep.recents.di.RecentsDependencies
-import com.android.quickstep.recents.di.get
-import com.android.quickstep.task.thumbnail.TaskOverlayUiState
-import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
-import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
-import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
-import com.android.systemui.shared.recents.model.Task
-import kotlinx.coroutines.CoroutineName
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.dropWhile
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.launch
-
-/**
- * Helper for [TaskOverlayFactory.TaskOverlay] to interact with [TaskOverlayViewModel], this helper
- * should merge with [TaskOverlayFactory.TaskOverlay] when it's migrated to MVVM.
- */
-class TaskOverlayHelper(val task: Task, val overlay: TaskOverlayFactory.TaskOverlay<*>) {
- private val scope = overlay.taskView.context
- private val recentsCoroutineScope: CoroutineScope = RecentsDependencies.get(scope)
- private val dispatcherProvider: DispatcherProvider = RecentsDependencies.get(scope)
- private lateinit var overlayInitializedScope: CoroutineScope
- var uiState: TaskOverlayUiState = Disabled
-
- private lateinit var viewModel: TaskOverlayViewModel
-
- // TODO(b/331753115): TaskOverlay should listen for state changes and react.
- val enabledState: Enabled
- get() = uiState as Enabled
-
- private val snapshotLayoutChangeListener = OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
- (uiState as? Enabled)?.let { initOverlay(it) }
- }
-
- fun getThumbnailMatrix() = getThumbnailPositionState().matrix
-
- fun getThumbnailPositionState() =
- viewModel.getThumbnailPositionState(
- overlay.snapshotView.width,
- overlay.snapshotView.height,
- overlay.snapshotView.isLayoutRtl,
- )
-
- fun init() {
- overlayInitializedScope =
- CoroutineScope(
- SupervisorJob() + Dispatchers.Main.immediate + CoroutineName("TaskOverlayHelper")
- )
- viewModel =
- TaskOverlayViewModel(
- task = task,
- recentsViewData = RecentsDependencies.get(scope),
- getThumbnailPositionUseCase = RecentsDependencies.get(scope),
- recentTasksRepository = RecentsDependencies.get(scope),
- dispatcherProvider = RecentsDependencies.get(scope),
- )
- viewModel.overlayState
- .dropWhile { it == Disabled }
- .flowOn(dispatcherProvider.background)
- .onEach {
- uiState = it
- if (it is Enabled) {
- initOverlay(it)
- } else {
- reset()
- }
- }
- .launchIn(overlayInitializedScope)
- overlay.snapshotView.addOnLayoutChangeListener(snapshotLayoutChangeListener)
- }
-
- private fun initOverlay(enabledState: Enabled) {
- if (DEBUG) {
- Log.d(TAG, "initOverlay - taskId: ${task.key.id}, thumbnail: ${enabledState.thumbnail}")
- }
- with(getThumbnailPositionState()) {
- overlay.initOverlay(task, enabledState.thumbnail, matrix, isRotated)
- }
- }
-
- private fun reset() {
- if (DEBUG) {
- Log.d(TAG, "reset - taskId: ${task.key.id}")
- }
- overlay.reset()
- }
-
- fun destroy() {
- recentsCoroutineScope.launch(dispatcherProvider.background) {
- overlayInitializedScope.cancel("TaskOverlay being destroyed")
- }
- uiState = Disabled
- overlay.snapshotView.removeOnLayoutChangeListener(snapshotLayoutChangeListener)
- reset()
- }
-
- companion object {
- private const val TAG = "TaskOverlayHelper"
- private const val DEBUG = false
- }
-}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
deleted file mode 100644
index 9bff3ac..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.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.domain.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
-import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
-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 */
-class TaskOverlayViewModel(
- private val task: Task,
- recentsViewData: RecentsViewData,
- private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
- private val recentTasksRepository: RecentTasksRepository,
- dispatcherProvider: DispatcherProvider,
-) {
- val overlayState =
- combine(
- recentsViewData.overlayEnabled,
- recentsViewData.settledFullyVisibleTaskIds
- .map { it.contains(task.key.id) }
- .distinctUntilChanged(),
- recentTasksRepository.getThumbnailById(task.key.id),
- ) { isOverlayEnabled, isFullyVisible, thumbnailData ->
- if (isOverlayEnabled && isFullyVisible) {
- Enabled(
- isRealSnapshot = (thumbnailData?.isRealSnapshot ?: false) && !task.isLocked,
- thumbnailData?.thumbnail,
- )
- } else {
- Disabled
- }
- }
- .distinctUntilChanged()
- .flowOn(dispatcherProvider.background)
-
- fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
- val thumbnailPositionState =
- getThumbnailPositionUseCase(
- thumbnailData = recentTasksRepository.getCurrentThumbnailById(task.key.id),
- width = width,
- height = height,
- isRtl = isRtl,
- )
- return ThumbnailPositionState(
- thumbnailPositionState.matrix,
- thumbnailPositionState.isRotated,
- )
- }
-
- data class ThumbnailPositionState(val matrix: Matrix, val isRotated: Boolean)
-}
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 6d7ae70..ec6d1c4 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -18,6 +18,7 @@
import android.graphics.Bitmap
import android.graphics.Matrix
+import android.util.Log
import android.view.View
import android.view.View.OnClickListener
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
@@ -26,6 +27,7 @@
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.ViewUtils.addAccessibleChildToList
+import com.android.quickstep.recents.domain.usecase.ThumbnailPosition
import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper
import com.android.quickstep.recents.ui.viewmodel.TaskData
import com.android.quickstep.task.thumbnail.TaskContentView
@@ -54,6 +56,8 @@
taskOverlayFactory: TaskOverlayFactory,
) {
val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
+ var thumbnailPosition: ThumbnailPosition? = null
+ private var overlayEnabledStatus = false
init {
if (enableRefactorTaskThumbnail()) {
@@ -101,14 +105,13 @@
if (!enableRefactorTaskThumbnail()) {
thumbnailViewDeprecated.bind(task, overlay, taskView)
}
- overlay.init()
}
fun destroy() {
digitalWellBeingToast?.destroy()
taskContentView.scaleX = 1f
taskContentView.scaleY = 1f
- overlay.destroy()
+ overlay.reset()
if (enableRefactorTaskThumbnail()) {
isThumbnailValid = false
thumbnailData = null
@@ -124,6 +127,34 @@
}
}
+ fun setOverlayEnabled(enabled: Boolean, thumbnailPosition: ThumbnailPosition?) {
+ if (enableRefactorTaskThumbnail()) {
+ if (overlayEnabledStatus != enabled || this.thumbnailPosition != thumbnailPosition) {
+ overlayEnabledStatus = enabled
+
+ refreshOverlay(thumbnailPosition)
+ }
+ }
+ }
+
+ fun refreshOverlay(thumbnailPosition: ThumbnailPosition?) {
+ this.thumbnailPosition = thumbnailPosition
+ when {
+ !overlayEnabledStatus -> overlay.reset()
+ thumbnailPosition == null -> {
+ Log.e(TAG, "Thumbnail position was null during overlay refresh", Exception())
+ overlay.reset()
+ }
+ else ->
+ overlay.initOverlay(
+ task,
+ thumbnailData?.thumbnail,
+ thumbnailPosition.matrix,
+ thumbnailPosition.isRotated,
+ )
+ }
+ }
+
fun addChildForAccessibility(outChildren: ArrayList<View>) {
addAccessibleChildToList(iconView.asView(), outChildren)
addAccessibleChildToList(snapshotView, outChildren)
@@ -144,6 +175,7 @@
state?.taskId,
)
thumbnailData = if (state is TaskData.Data) state.thumbnailData else null
+ overlay.setThumbnailState(thumbnailData)
}
fun updateTintAmount(tintAmount: Float) {
@@ -184,4 +216,8 @@
fun updateThumbnailMatrix(matrix: Matrix) {
thumbnailView.setImageMatrix(matrix)
}
+
+ companion object {
+ const val TAG = "TaskContainer"
+ }
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.kt b/quickstep/src/com/android/quickstep/views/TaskMenuView.kt
index 200fd52..7c762f4 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.kt
@@ -115,13 +115,9 @@
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var heightMeasure = heightMeasureSpec
- if (!(enableOverviewIconMenu() && taskView.isOnGridBottomRow())) {
- // TODO(b/326952853): Cap menu height for grid bottom row in a way that doesn't break
- // additionalTranslationY.
- val maxMenuHeight = calculateMaxHeight()
- if (MeasureSpec.getSize(heightMeasure) > maxMenuHeight) {
- heightMeasure = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST)
- }
+ val maxMenuHeight = calculateMaxHeight()
+ if (MeasureSpec.getSize(heightMeasure) > maxMenuHeight) {
+ heightMeasure = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST)
}
super.onMeasure(widthMeasureSpec, heightMeasure)
}
@@ -364,15 +360,20 @@
* view will scroll. The maximum menu size will sit inside the task with a margin on the top and
* bottom.
*/
- private fun calculateMaxHeight(): Int {
- val taskInsetMargin = resources.getDimension(R.dimen.task_card_margin)
- return taskView.pagedOrientationHandler.getTaskMenuHeight(
- taskInsetMargin,
- recentsViewContainer.deviceProfile,
- translationX,
- translationY,
+ private fun calculateMaxHeight(): Int =
+ taskView.pagedOrientationHandler.getTaskMenuHeight(
+ taskInsetMargin = resources.getDimension(R.dimen.task_card_margin), // taskInsetMargin
+ deviceProfile = recentsViewContainer.deviceProfile,
+ taskMenuX = translationX,
+ taskMenuY =
+ when {
+ !enableOverviewIconMenu() -> translationY
+ // Bottom menu can translate up to show more options. So we use the min
+ // translation allowed to calculate its max height.
+ taskView.isOnGridBottomRow() -> minMenuTop
+ else -> menuTranslationYBeforeOpen
+ },
)
- }
private fun setOnClosingStartCallback(onClosingStartCallback: Runnable?) {
this.onClosingStartCallback = onClosingStartCallback
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index c878d56..b7f1d1d 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -82,6 +82,7 @@
import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.recents.di.get
import com.android.quickstep.recents.di.inject
+import com.android.quickstep.recents.domain.usecase.ThumbnailPosition
import com.android.quickstep.recents.ui.viewmodel.TaskData
import com.android.quickstep.recents.ui.viewmodel.TaskTileUiState
import com.android.quickstep.recents.ui.viewmodel.TaskViewModel
@@ -775,11 +776,13 @@
},
)
updateThumbnailValidity(container)
- updateThumbnailMatrix(
- container = container,
- width = container.thumbnailView.width,
- height = container.thumbnailView.height,
- )
+ val thumbnailPosition =
+ updateThumbnailMatrix(
+ container = container,
+ width = container.thumbnailView.width,
+ height = container.thumbnailView.height,
+ )
+ container.setOverlayEnabled(state.taskOverlayEnabled, thumbnailPosition)
if (enableOverviewIconMenu()) {
setIconState(container, containerState)
@@ -808,11 +811,16 @@
* @param width The desired width of the thumbnail's container.
* @param height The desired height of the thumbnail's container.
*/
- private fun updateThumbnailMatrix(container: TaskContainer, width: Int, height: Int) {
+ private fun updateThumbnailMatrix(
+ container: TaskContainer,
+ width: Int,
+ height: Int,
+ ): ThumbnailPosition? {
val thumbnailPosition =
viewModel?.getThumbnailPosition(container.thumbnailData, width, height, isLayoutRtl)
- ?: return
+ ?: return null
container.updateThumbnailMatrix(thumbnailPosition.matrix)
+ return thumbnailPosition
}
override fun onDetachedFromWindow() {
@@ -877,7 +885,8 @@
thumbnailFullscreenParams.currentCornerRadius
container.taskContentView.doOnSizeChange { width, height ->
updateThumbnailValidity(container)
- updateThumbnailMatrix(container, width, height)
+ val thumbnailPosition = updateThumbnailMatrix(container, width, height)
+ container.refreshOverlay(thumbnailPosition)
}
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPopupControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPopupControllerTest.kt
new file mode 100644
index 0000000..6bb3205
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPopupControllerTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.taskbar
+
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.BubbleTextView
+import com.android.launcher3.Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR
+import com.android.launcher3.Flags.FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU
+import com.android.launcher3.R
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatWorkspaceItem
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.android.quickstep.util.GroupTask
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+@DisableFlags(FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR, FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU)
+class TaskbarPopupControllerTest {
+ @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
+
+ @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create()
+
+ @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+ @InjectController lateinit var popupController: TaskbarPopupController
+
+ private val taskbarContext: TaskbarActivityContext
+ get() = taskbarUnitTestRule.activityContext
+
+ private lateinit var taskbarView: TaskbarView
+ private lateinit var hotseatIcon: BubbleTextView
+ private lateinit var recentTaskIcon: BubbleTextView
+
+ @Before
+ fun setup() {
+ taskbarContext.controllers.uiController.init(taskbarContext.controllers)
+ runOnMainSync { taskbarView = taskbarContext.dragLayer.findViewById(R.id.taskbar_view) }
+
+ val hotseatItems = arrayOf(createHotseatWorkspaceItem())
+ val recentItems = createRecents(2)
+ runOnMainSync {
+ taskbarView.updateItems(hotseatItems, recentItems)
+ hotseatIcon =
+ taskbarView.iconViews.filterIsInstance<BubbleTextView>().first {
+ it.tag is WorkspaceItemInfo
+ }
+ recentTaskIcon =
+ taskbarView.iconViews.filterIsInstance<BubbleTextView>().first {
+ it.tag is GroupTask
+ }
+ }
+ }
+
+ @Test
+ fun showForIcon_hotseatItem() {
+ assertThat(hasPopupMenu()).isFalse()
+ runOnMainSync { popupController.showForIcon(hotseatIcon) }
+ assertThat(hasPopupMenu()).isTrue()
+ }
+
+ @Test
+ fun showForIcon_recentTask() {
+ assertThat(hasPopupMenu()).isFalse()
+ runOnMainSync { popupController.showForIcon(recentTaskIcon) }
+ assertThat(hasPopupMenu()).isTrue()
+ }
+
+ private fun hasPopupMenu(): Boolean {
+ return AbstractFloatingView.hasOpenView(
+ taskbarContext,
+ AbstractFloatingView.TYPE_ACTION_POPUP,
+ )
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
index df70b10..e52aacf 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
@@ -46,12 +46,14 @@
/** Creates an array of fake hotseat items. */
fun createHotseatItems(size: Int): Array<ItemInfo> {
- return Array(size) {
- WorkspaceItemInfo(
- AppInfo(TEST_COMPONENT, "Test App $it", Process.myUserHandle(), Intent())
- )
- .apply { id = it }
- }
+ return Array(size) { createHotseatWorkspaceItem(it) }
+ }
+
+ fun createHotseatWorkspaceItem(id: Int = 0): WorkspaceItemInfo {
+ return WorkspaceItemInfo(
+ AppInfo(TEST_COMPONENT, "Test App $id", Process.myUserHandle(), Intent())
+ )
+ .apply { this.id = id }
}
/** Creates a list of fake recent tasks. */
@@ -75,13 +77,13 @@
}
/** A `Truth` [Subject] with extensions for verifying [TaskbarView]. */
-class TaskbarViewSubject(failureMetadata: FailureMetadata, private val view: TaskbarView) :
+class TaskbarViewSubject(failureMetadata: FailureMetadata, private val view: TaskbarView?) :
Subject(failureMetadata, view) {
/** Verifies that the types of icons match [expectedTypes] in order. */
fun hasIconTypes(vararg expectedTypes: TaskbarIconType) {
val actualTypes =
- view.iconViews.map {
+ view?.iconViews?.map {
when (it) {
view.allAppsButtonContainer -> ALL_APPS
view.taskbarDividerViewContainer -> DIVIDER
@@ -100,7 +102,7 @@
/** Verifies that recents from [startIndex] have IDs that match [expectedIds] in order. */
fun hasRecentsOrder(startIndex: Int, expectedIds: List<Int>) {
val actualIds =
- view.iconViews.slice(startIndex..<startIndex + expectedIds.size).flatMap {
+ view?.iconViews?.slice(startIndex..<startIndex + expectedIds.size)?.flatMap {
assertThat(it.tag).isInstanceOf(GroupTask::class.java)
(it.tag as GroupTask).tasks.map { task -> task.key.id }
}
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 a456fb9..61a6975 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
@@ -20,8 +20,10 @@
import android.graphics.Color
import android.graphics.Path
import android.graphics.PointF
+import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
+import android.view.MotionEvent
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
@@ -35,10 +37,13 @@
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.R
+import com.android.launcher3.taskbar.TaskbarInsetsController
import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow
import com.android.launcher3.taskbar.bubbles.BubbleBarParentViewHeightUpdateNotifier
import com.android.launcher3.taskbar.bubbles.BubbleBarView
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
import com.android.launcher3.taskbar.bubbles.BubbleView
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage
@@ -46,8 +51,10 @@
import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks
import com.android.launcher3.taskbar.bubbles.flyout.FlyoutScheduler
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState
import com.android.wm.shell.shared.animation.PhysicsAnimator
import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
import com.android.wm.shell.shared.bubbles.BubbleInfo
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Semaphore
@@ -56,13 +63,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.kotlin.any
-import org.mockito.kotlin.atLeastOnce
-import org.mockito.kotlin.eq
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
-import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -78,7 +78,7 @@
private lateinit var bubble: BubbleBarBubble
private lateinit var bubbleBarView: BubbleBarView
private lateinit var flyoutContainer: FrameLayout
- private lateinit var bubbleStashController: BubbleStashController
+ private lateinit var bubbleStashController: FakeBubbleStashController
private lateinit var flyoutController: BubbleBarFlyoutController
private val emptyRunnable = Runnable {}
@@ -89,6 +89,7 @@
fun setUp() {
animatorScheduler = TestBubbleBarViewAnimatorScheduler()
bubbleBarParentViewController = TestBubbleBarParentViewHeightUpdateNotifier()
+ bubbleStashController = FakeBubbleStashController()
PhysicsAnimatorTestUtils.prepareForTest()
setupFlyoutController()
}
@@ -96,11 +97,10 @@
@Test
fun animateBubbleInForStashed() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val animator =
BubbleBarViewAnimator(
@@ -147,17 +147,16 @@
assertThat(handle.translationY).isEqualTo(0)
assertThat(bubbleBarView.alpha).isEqualTo(0)
assertThat(animator.isAnimating).isFalse()
- verify(bubbleStashController).stashBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isTrue()
}
@Test
fun animateBubbleInForStashed_tapAnimatingBubble() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val animator =
BubbleBarViewAnimator(
@@ -187,7 +186,7 @@
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(animator.isAnimating).isTrue()
- verify(bubbleStashController, atLeastOnce()).updateTaskbarTouchRegion()
+ assertThat(bubbleStashController.taskbarTouchRegionUpdated).isTrue()
assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
waitForFlyoutToShow()
@@ -211,11 +210,10 @@
@Test
fun animateBubbleInForStashed_touchTaskbarArea_whileShowing() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val animator =
BubbleBarViewAnimator(
@@ -241,7 +239,6 @@
// verify the hide bubble animation is pending
assertThat(animatorScheduler.delayedBlock).isNotNull()
- whenever(bubbleStashController.isStashed).thenReturn(true)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.onStashStateChangingWhileAnimating()
}
@@ -255,9 +252,10 @@
// verify that the hide animation was canceled
assertThat(animatorScheduler.delayedBlock).isNull()
assertThat(animator.isAnimating).isFalse()
+ assertThat(bubbleStashController.animationInterrupted).isTrue()
assertThat(bubbleBarView.scaleX).isEqualTo(1)
assertThat(bubbleBarView.scaleY).isEqualTo(1)
- verify(bubbleStashController).onNewBubbleAnimationInterrupted(eq(true), any())
+ assertThat(bubbleStashController.isStashed).isTrue()
// PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
// again
@@ -268,11 +266,10 @@
@Test
fun animateBubbleInForStashed_touchTaskbarArea_whileHiding() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val animator =
BubbleBarViewAnimator(
@@ -310,7 +307,7 @@
}
assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
assertThat(animator.isAnimating).isFalse()
- verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
+ assertThat(bubbleStashController.animationInterrupted).isTrue()
// PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
// again
@@ -321,11 +318,10 @@
@Test
fun animateBubbleInForStashed_showAnimationCanceled() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val animator =
BubbleBarViewAnimator(
@@ -359,11 +355,10 @@
@Test
fun animateBubbleInForStashed_autoExpanding() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
var notifiedExpanded = false
val onExpanded = Runnable { notifiedExpanded = true }
@@ -399,18 +394,17 @@
// verify there is no hide animation
assertThat(animatorScheduler.delayedBlock).isNull()
- verify(bubbleStashController).showBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isFalse()
assertThat(notifiedExpanded).isTrue()
}
@Test
fun animateBubbleInForStashed_expandedWhileAnimatingIn() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
var notifiedExpanded = false
val onExpanded = Runnable { notifiedExpanded = true }
@@ -459,11 +453,10 @@
@Test
fun animateBubbleInForStashed_expandedWhileFullyIn() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
var notifiedExpanded = false
val onExpanded = Runnable { notifiedExpanded = true }
@@ -515,13 +508,11 @@
@Test
fun animateToInitialState_inApp() {
setUpBubbleBar()
- setUpBubbleStashController()
- whenever(bubbleStashController.bubbleBarTranslationY)
- .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ bubbleStashController.launcherState = BubbleLauncherState.IN_APP
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
@@ -570,19 +561,17 @@
assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
assertThat(notifiedBubbleBarVisible).isTrue()
- verify(bubbleStashController).stashBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isTrue()
}
@Test
fun animateToInitialState_whileDragging_inApp() {
setUpBubbleBar()
- setUpBubbleStashController()
- whenever(bubbleStashController.bubbleBarTranslationY)
- .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ bubbleStashController.launcherState = BubbleLauncherState.IN_APP
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
@@ -634,20 +623,16 @@
assertThat(handle.translationY).isEqualTo(0)
assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
assertThat(notifiedBubbleBarVisible).isTrue()
-
- verify(bubbleStashController, never()).stashBubbleBarImmediate()
}
@Test
fun animateToInitialState_inApp_autoExpanding() {
setUpBubbleBar()
- setUpBubbleStashController()
- whenever(bubbleStashController.bubbleBarTranslationY)
- .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ bubbleStashController.launcherState = BubbleLauncherState.IN_APP
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
@@ -677,16 +662,14 @@
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(animatorScheduler.delayedBlock).isNull()
- verify(bubbleStashController).showBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isFalse()
assertThat(notifiedExpanded).isTrue()
}
@Test
fun animateToInitialState_inHome() {
setUpBubbleBar()
- setUpBubbleStashController()
- whenever(bubbleStashController.bubbleBarTranslationY)
- .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ bubbleStashController.launcherState = BubbleLauncherState.HOME
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
@@ -725,15 +708,13 @@
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
- verify(bubbleStashController).showBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isFalse()
}
@Test
fun animateToInitialState_expandedWhileAnimatingIn() {
setUpBubbleBar()
- setUpBubbleStashController()
- whenever(bubbleStashController.bubbleBarTranslationY)
- .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ bubbleStashController.launcherState = BubbleLauncherState.HOME
var notifiedExpanded = false
val onExpanded = Runnable { notifiedExpanded = true }
@@ -775,16 +756,14 @@
verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
assertThat(animator.isAnimating).isFalse()
- verify(bubbleStashController).showBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isFalse()
assertThat(notifiedExpanded).isTrue()
}
@Test
fun animateToInitialState_expandedWhileFullyIn() {
setUpBubbleBar()
- setUpBubbleStashController()
- whenever(bubbleStashController.bubbleBarTranslationY)
- .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ bubbleStashController.launcherState = BubbleLauncherState.HOME
var notifiedExpanded = false
val onExpanded = Runnable { notifiedExpanded = true }
@@ -831,9 +810,7 @@
@Test
fun animateBubbleBarForCollapsed() {
setUpBubbleBar()
- setUpBubbleStashController()
- whenever(bubbleStashController.bubbleBarTranslationY)
- .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ bubbleStashController.launcherState = BubbleLauncherState.HOME
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
@@ -877,16 +854,13 @@
assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
// the bubble bar translation y should be back to its initial value
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
-
- verify(bubbleStashController).showBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isFalse()
}
@Test
fun animateBubbleBarForCollapsed_autoExpanding() {
setUpBubbleBar()
- setUpBubbleStashController()
- whenever(bubbleStashController.bubbleBarTranslationY)
- .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ bubbleStashController.launcherState = BubbleLauncherState.HOME
val semaphore = Semaphore(0)
var notifiedExpanded = false
@@ -933,16 +907,14 @@
assertThat(animator.isAnimating).isFalse()
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
- verify(bubbleStashController).showBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isFalse()
assertThat(notifiedExpanded).isTrue()
}
@Test
fun animateBubbleBarForCollapsed_expandingWhileAnimatingIn() {
setUpBubbleBar()
- setUpBubbleStashController()
- whenever(bubbleStashController.bubbleBarTranslationY)
- .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ bubbleStashController.launcherState = BubbleLauncherState.HOME
val semaphore = Semaphore(0)
var notifiedExpanded = false
@@ -1001,16 +973,14 @@
assertThat(animator.isAnimating).isFalse()
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
assertThat(bubbleBarView.isExpanded).isTrue()
- verify(bubbleStashController).showBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isFalse()
assertThat(notifiedExpanded).isTrue()
}
@Test
fun animateBubbleBarForCollapsed_expandingWhileFullyIn() {
setUpBubbleBar()
- setUpBubbleStashController()
- whenever(bubbleStashController.bubbleBarTranslationY)
- .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ bubbleStashController.launcherState = BubbleLauncherState.HOME
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
@@ -1064,18 +1034,17 @@
assertThat(animator.isAnimating).isFalse()
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
assertThat(bubbleBarView.isExpanded).isTrue()
- verify(bubbleStashController).showBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isFalse()
assertThat(notifiedExpanded).isTrue()
}
@Test
fun interruptAnimation_whileAnimatingIn() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val animator =
BubbleBarViewAnimator(
@@ -1136,17 +1105,16 @@
assertThat(handle.translationY).isEqualTo(0)
assertThat(bubbleBarView.alpha).isEqualTo(0)
assertThat(animator.isAnimating).isFalse()
- verify(bubbleStashController).stashBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isTrue()
}
@Test
fun interruptAnimation_whileIn() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val animator =
BubbleBarViewAnimator(
@@ -1213,17 +1181,16 @@
assertThat(handle.translationY).isEqualTo(0)
assertThat(bubbleBarView.alpha).isEqualTo(0)
assertThat(animator.isAnimating).isFalse()
- verify(bubbleStashController).stashBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isTrue()
}
@Test
fun interruptAnimation_whileAnimatingOut_whileCollapsingFlyout() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val animator =
BubbleBarViewAnimator(
@@ -1301,17 +1268,16 @@
assertThat(handle.translationY).isEqualTo(0)
assertThat(bubbleBarView.alpha).isEqualTo(0)
assertThat(animator.isAnimating).isFalse()
- verify(bubbleStashController).stashBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isTrue()
}
@Test
fun interruptAnimation_whileAnimatingOut_barToHandle() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val animator =
BubbleBarViewAnimator(
@@ -1416,17 +1382,16 @@
assertThat(handle.translationY).isEqualTo(0)
assertThat(bubbleBarView.alpha).isEqualTo(0)
assertThat(animator.isAnimating).isFalse()
- verify(bubbleStashController).stashBubbleBarImmediate()
+ assertThat(bubbleStashController.isStashed).isTrue()
}
@Test
fun interruptForIme() {
setUpBubbleBar()
- setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
- whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+ bubbleStashController.handleAnimator = handleAnimator
val animator =
BubbleBarViewAnimator(
@@ -1457,7 +1422,8 @@
// verify that the hide animation was canceled
assertThat(animatorScheduler.delayedBlock).isNull()
assertThat(animator.isAnimating).isFalse()
- verify(bubbleStashController).onNewBubbleAnimationInterrupted(eq(true), any())
+ assertThat(bubbleStashController.animationInterrupted).isTrue()
+ assertThat(bubbleStashController.isStashed).isTrue()
// PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
// again
@@ -1510,17 +1476,6 @@
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
}
- private fun setUpBubbleStashController() {
- bubbleStashController = mock<BubbleStashController>()
- whenever(bubbleStashController.isStashed).thenReturn(true)
- whenever(bubbleStashController.getDiffBetweenHandleAndBarCenters())
- .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
- whenever(bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation())
- .thenReturn(HANDLE_TRANSLATION)
- whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
- .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
- }
-
private fun setupFlyoutController() {
flyoutContainer = FrameLayout(context)
val flyoutPositioner =
@@ -1627,6 +1582,91 @@
timesInvoked++
}
}
+
+ private class FakeBubbleStashController : BubbleStashController {
+
+ var handleAnimator: PhysicsAnimator<View>? = null
+ var taskbarTouchRegionUpdated = false
+ private set
+
+ var animationInterrupted = false
+ private set
+
+ private var _isStashed = true
+
+ override var launcherState = BubbleLauncherState.HOME
+ override val isStashed: Boolean
+ get() = _isStashed
+
+ override var bubbleBarVerticalCenterForHome = 0
+ override var isSysuiLocked = false
+ override val isTransientTaskBar = true
+ override val hasHandleView = true
+ override val bubbleBarTranslationYForTaskbar = BAR_TRANSLATION_Y_FOR_TASKBAR
+ override val bubbleBarTranslationYForHotseat = BAR_TRANSLATION_Y_FOR_HOTSEAT
+ override var inAppDisplayOverrideProgress = 0f
+
+ override fun init(
+ taskbarInsetsController: TaskbarInsetsController,
+ bubbleBarViewController: BubbleBarViewController,
+ bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
+ controllersAfterInitAction: BubbleStashController.ControllersAfterInitAction,
+ ) {}
+
+ override fun showBubbleBarImmediate() {
+ _isStashed = false
+ }
+
+ override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) {
+ _isStashed = false
+ }
+
+ override fun stashBubbleBarImmediate() {
+ _isStashed = true
+ }
+
+ override fun getTouchableHeight() = 100
+
+ override fun isBubbleBarVisible() = true
+
+ override fun onNewBubbleAnimationInterrupted(
+ isStashed: Boolean,
+ bubbleBarTranslationY: Float,
+ ) {
+ _isStashed = isStashed
+ animationInterrupted = true
+ }
+
+ override fun isEventOverBubbleBarViews(ev: MotionEvent) = false
+
+ override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) {}
+
+ override fun stashBubbleBar() {
+ _isStashed = true
+ }
+
+ override fun showBubbleBar(expandBubbles: Boolean, bubbleBarGesture: Boolean) {
+ _isStashed = false
+ }
+
+ override fun getDiffBetweenHandleAndBarCenters() = DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS
+
+ override fun getStashedHandleTranslationForNewBubbleAnimation() = HANDLE_TRANSLATION
+
+ override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator<View>? {
+ return handleAnimator
+ }
+
+ override fun updateTaskbarTouchRegion() {
+ taskbarTouchRegionUpdated = true
+ }
+
+ override fun setHandleTranslationY(translationY: Float) {}
+
+ override fun getHandleTranslationY() = 0f
+
+ override fun getHandleBounds(bounds: Rect) {}
+ }
}
private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
index a97ef0c..18b9fe9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
@@ -66,16 +66,7 @@
@Before
fun setUp() {
- sut =
- TaskViewModel(
- taskViewType = TaskViewType.SINGLE,
- recentsViewData = recentsViewData,
- getTaskUseCase = getTaskUseCase,
- getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
- isThumbnailValidUseCase = isThumbnailValidUseCase,
- getThumbnailPositionUseCase = getThumbnailPositionUseCase,
- dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
- )
+ sut = createTaskViewModel(TaskViewType.SINGLE)
whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) })
whenever(getTaskUseCase.invoke(TASK_MODEL_2.id)).thenReturn(flow { emit(TASK_MODEL_2) })
whenever(getTaskUseCase.invoke(TASK_MODEL_3.id)).thenReturn(flow { emit(TASK_MODEL_3) })
@@ -93,6 +84,7 @@
isLiveTile = false,
hasHeader = false,
sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME,
+ taskOverlayEnabled = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -139,6 +131,7 @@
isLiveTile = false,
hasHeader = false,
sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME,
+ taskOverlayEnabled = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -161,6 +154,7 @@
isLiveTile = true,
hasHeader = false,
sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME,
+ taskOverlayEnabled = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -183,6 +177,7 @@
isLiveTile = false,
hasHeader = false,
sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME,
+ taskOverlayEnabled = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -204,6 +199,7 @@
isLiveTile = false,
hasHeader = false,
sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME,
+ taskOverlayEnabled = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -221,6 +217,7 @@
isLiveTile = false,
hasHeader = false,
sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME,
+ taskOverlayEnabled = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -235,11 +232,64 @@
isLiveTile = false,
hasHeader = false,
sysUiStatusNavFlags = FLAGS_APPEARANCE_DEFAULT,
+ taskOverlayEnabled = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@Test
+ fun taskOverlayEnabled_when_OverlayIsEnabledForVisibleSingleTask() =
+ testScope.runTest {
+ sut.bind(TASK_MODEL_1.id)
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(1)
+
+ assertThat(sut.state.first().taskOverlayEnabled).isTrue()
+ }
+
+ @Test
+ fun taskOverlayDisabled_when_usingGroupedTask() =
+ testScope.runTest {
+ sut = createTaskViewModel(TaskViewType.GROUPED)
+ sut.bind(TASK_MODEL_1.id)
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(1)
+
+ assertThat(sut.state.first().taskOverlayEnabled).isFalse()
+ }
+
+ @Test
+ fun taskOverlayDisabled_when_usingDesktopTask() =
+ testScope.runTest {
+ sut = createTaskViewModel(TaskViewType.DESKTOP)
+ sut.bind(TASK_MODEL_1.id)
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(1)
+
+ assertThat(sut.state.first().taskOverlayEnabled).isFalse()
+ }
+
+ @Test
+ fun taskOverlayDisabled_when_OverlayIsEnabledForInvisibleTask() =
+ testScope.runTest {
+ sut.bind(TASK_MODEL_1.id)
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(2)
+
+ assertThat(sut.state.first().taskOverlayEnabled).isFalse()
+ }
+
+ @Test
+ fun taskOverlayDisabled_when_OverlayIsDisabledForVisibleTask() =
+ testScope.runTest {
+ sut.bind(TASK_MODEL_1.id)
+ recentsViewData.overlayEnabled.value = false
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(1)
+
+ assertThat(sut.state.first().taskOverlayEnabled).isFalse()
+ }
+
+ @Test
fun shouldShowSplash_calls_useCase() {
sut.isThumbnailValid(null, 0, 0)
verify(isThumbnailValidUseCase).invoke(anyOrNull(), anyInt(), anyInt())
@@ -256,6 +306,17 @@
isLocked = isLocked,
)
+ private fun createTaskViewModel(taskViewType: TaskViewType) =
+ TaskViewModel(
+ taskViewType = taskViewType,
+ recentsViewData = recentsViewData,
+ getTaskUseCase = getTaskUseCase,
+ getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
+ isThumbnailValidUseCase = isThumbnailValidUseCase,
+ getThumbnailPositionUseCase = getThumbnailPositionUseCase,
+ dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
+ )
+
private companion object {
const val INVALID_TASK_ID = -1
const val FLAGS_APPEARANCE_LIGHT_THEME = FLAG_LIGHT_STATUS or FLAG_LIGHT_NAV
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 64f67cd..a15c130 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -445,7 +445,6 @@
<dimen name="taskbar_running_app_indicator_height">0dp</dimen>
<dimen name="taskbar_running_app_indicator_width">0dp</dimen>
<dimen name="taskbar_running_app_indicator_top_margin">0dp</dimen>
- <dimen name="taskbar_minimized_app_indicator_width">0dp</dimen>
<!-- Transient taskbar (placeholders to compile in Launcher3 without Quickstep) -->
<dimen name="transient_taskbar_padding">0dp</dimen>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 7b656d7..783e82c 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -20,6 +20,7 @@
import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
import static android.text.Layout.Alignment.ALIGN_NORMAL;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.launcher3.BubbleTextView.RunningAppState.RUNNING;
import static com.android.launcher3.BubbleTextView.RunningAppState.NOT_RUNNING;
import static com.android.launcher3.BubbleTextView.RunningAppState.MINIMIZED;
@@ -37,6 +38,7 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
@@ -130,6 +132,9 @@
StringMatcherUtility.StringMatcher.getInstance();
private static final int BOLD_TEXT_ADJUSTMENT = FONT_WEIGHT_BOLD - FONT_WEIGHT_NORMAL;
+ public static final int LINE_INDICATOR_ANIM_DURATION = 150;
+ private static final float MINIMIZED_APP_INDICATOR_SCALE = 0.5f;
+
private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};
private float mScaleForReorderBounce = 1f;
@@ -165,6 +170,36 @@
}
};
+ private static final Property<BubbleTextView, Integer> LINE_INDICATOR_COLOR_PROPERTY =
+ new Property<>(Integer.class, "lineIndicatorColor") {
+
+ @Override
+ public Integer get(BubbleTextView bubbleTextView) {
+ return bubbleTextView.mLineIndicatorColor;
+ }
+
+ @Override
+ public void set(BubbleTextView bubbleTextView, Integer color) {
+ bubbleTextView.mLineIndicatorColor = color;
+ bubbleTextView.invalidate();
+ }
+ };
+
+ private static final Property<BubbleTextView, Float> LINE_INDICATOR_SCALE_PROPERTY =
+ new Property<>(Float.TYPE, "lineIndicatorScale") {
+
+ @Override
+ public Float get(BubbleTextView bubbleTextView) {
+ return bubbleTextView.mLineIndicatorScale;
+ }
+
+ @Override
+ public void set(BubbleTextView bubbleTextView, Float scale) {
+ bubbleTextView.mLineIndicatorScale = scale;
+ bubbleTextView.invalidate();
+ }
+ };
+
private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
protected final ActivityContext mActivity;
private FastBitmapDrawable mIcon;
@@ -202,7 +237,6 @@
// These fields, related to showing running apps, are only used for Taskbar.
private final int mRunningAppIndicatorWidth;
- private final int mMinimizedAppIndicatorWidth;
private final int mRunningAppIndicatorHeight;
private final int mRunningAppIndicatorTopMargin;
private final Paint mRunningAppIndicatorPaint;
@@ -210,6 +244,12 @@
private RunningAppState mRunningAppState;
private final int mRunningAppIndicatorColor;
private final int mMinimizedAppIndicatorColor;
+ @ViewDebug.ExportedProperty(category = "launcher")
+ private int mLineIndicatorColor;
+ @ViewDebug.ExportedProperty(category = "launcher")
+ private float mLineIndicatorScale;
+ private int mLineIndicatorAnimStartDelay;
+ private Animator mLineIndicatorAnim;
private final String mMinimizedStateDescription;
private final String mRunningStateDescription;
@@ -294,8 +334,6 @@
mRunningAppIndicatorWidth =
getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_width);
- mMinimizedAppIndicatorWidth =
- getResources().getDimensionPixelSize(R.dimen.taskbar_minimized_app_indicator_width);
mRunningAppIndicatorHeight =
getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_height);
mRunningAppIndicatorTopMargin =
@@ -344,6 +382,11 @@
mForceHideDot = false;
setBackground(null);
+ mLineIndicatorColor = Color.TRANSPARENT;
+ mLineIndicatorScale = 0;
+ mLineIndicatorAnimStartDelay = 0;
+ cancelLineIndicatorAnim();
+
setTag(null);
if (mIconLoadRequest != null) {
mIconLoadRequest.cancel();
@@ -436,9 +479,50 @@
/** Updates whether the app this view represents is currently running. */
@UiThread
- public void updateRunningState(RunningAppState runningAppState) {
+ public void updateRunningState(RunningAppState runningAppState, boolean animate) {
+ if (runningAppState.equals(mRunningAppState)) {
+ return;
+ }
mRunningAppState = runningAppState;
- invalidate();
+ cancelLineIndicatorAnim();
+
+ int color = switch (mRunningAppState) {
+ case NOT_RUNNING -> Color.TRANSPARENT;
+ case RUNNING -> mRunningAppIndicatorColor;
+ case MINIMIZED -> mMinimizedAppIndicatorColor;
+ };
+ float scale = switch (mRunningAppState) {
+ case NOT_RUNNING -> 0;
+ case RUNNING -> 1;
+ case MINIMIZED -> MINIMIZED_APP_INDICATOR_SCALE;
+ };
+
+ if (!animate) {
+ mLineIndicatorColor = color;
+ mLineIndicatorScale = scale;
+ invalidate();
+ return;
+ }
+
+ AnimatorSet lineIndicatorAnim = new AnimatorSet();
+ mLineIndicatorAnim = lineIndicatorAnim;
+ Animator colorAnimator = ObjectAnimator.ofArgb(this, LINE_INDICATOR_COLOR_PROPERTY, color);
+ Animator scaleAnimator = ObjectAnimator.ofFloat(this, LINE_INDICATOR_SCALE_PROPERTY, scale);
+ lineIndicatorAnim.playTogether(colorAnimator, scaleAnimator);
+
+ lineIndicatorAnim.setInterpolator(EMPHASIZED);
+ lineIndicatorAnim.setStartDelay(mLineIndicatorAnimStartDelay);
+ lineIndicatorAnim.setDuration(LINE_INDICATOR_ANIM_DURATION).start();
+ }
+
+ public void setLineIndicatorAnimStartDelay(int lineIndicatorAnimStartDelay) {
+ mLineIndicatorAnimStartDelay = lineIndicatorAnimStartDelay;
+ }
+
+ private void cancelLineIndicatorAnim() {
+ if (mLineIndicatorAnim != null) {
+ mLineIndicatorAnim.cancel();
+ }
}
/**
@@ -790,19 +874,18 @@
/** Draws a line under the app icon if this is representing a running app in Desktop Mode. */
protected void drawRunningAppIndicatorIfNecessary(Canvas canvas) {
- if (mRunningAppState == NOT_RUNNING || mDisplay != DISPLAY_TASKBAR) {
+ if (mDisplay != DISPLAY_TASKBAR
+ || mLineIndicatorScale == 0
+ || mLineIndicatorColor == Color.TRANSPARENT) {
return;
}
getIconBounds(mRunningAppIconBounds);
Utilities.scaleRectAboutCenter(mRunningAppIconBounds, ICON_VISIBLE_AREA_FACTOR);
- final boolean isMinimized = mRunningAppState == MINIMIZED;
final int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin;
- final int indicatorWidth =
- isMinimized ? mMinimizedAppIndicatorWidth : mRunningAppIndicatorWidth;
+ final float indicatorWidth = mRunningAppIndicatorWidth * mLineIndicatorScale;
final float cornerRadius = mRunningAppIndicatorHeight / 2f;
- mRunningAppIndicatorPaint.setColor(
- isMinimized ? mMinimizedAppIndicatorColor : mRunningAppIndicatorColor);
+ mRunningAppIndicatorPaint.setColor(mLineIndicatorColor);
canvas.drawRoundRect(
mRunningAppIconBounds.centerX() - indicatorWidth / 2f,
diff --git a/tests/src/com/android/launcher3/util/rule/ZipFilesRule.kt b/tests/multivalentTests/src/com/android/launcher3/util/rule/ZipFilesRule.kt
similarity index 100%
rename from tests/src/com/android/launcher3/util/rule/ZipFilesRule.kt
rename to tests/multivalentTests/src/com/android/launcher3/util/rule/ZipFilesRule.kt