Merge "Change drawing in TTV to use Views rather than Canvas" into main
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 8984086..44d8a5c 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -20,6 +20,7 @@
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;
+import static com.android.launcher3.Flags.enablePredictiveBackGesture;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -33,6 +34,9 @@
import android.view.View;
import android.view.WindowInsetsController;
import android.view.WindowManager;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
+import android.window.OnBackInvokedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -124,6 +128,8 @@
/** A set of user ids that should be filtered out from the selected widgets. */
@NonNull
Set<Integer> mFilteredUserIds = new HashSet<>();
+ @Nullable
+ private WidgetsFullSheet mWidgetSheet;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -148,6 +154,18 @@
refreshAndBindWidgets();
}
+ @Override
+ protected void registerBackDispatcher() {
+ if (!enablePredictiveBackGesture()) {
+ super.registerBackDispatcher();
+ return;
+ }
+
+ getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+ OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+ new BackAnimationCallback());
+ }
+
private void parseIntentExtras() {
mTitle = getIntent().getStringExtra(EXTRA_PICKER_TITLE);
mDescription = getIntent().getStringExtra(EXTRA_PICKER_DESCRIPTION);
@@ -293,12 +311,12 @@
MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets));
}
- private void openWidgetsSheet() {
+ private void openWidgetsSheet() {
MAIN_EXECUTOR.execute(() -> {
- WidgetsFullSheet widgetSheet = WidgetsFullSheet.show(this, true);
- widgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription);
- widgetSheet.disableNavBarScrim(true);
- widgetSheet.addOnCloseListener(this::finish);
+ mWidgetSheet = WidgetsFullSheet.show(this, true);
+ mWidgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription);
+ mWidgetSheet.disableNavBarScrim(true);
+ mWidgetSheet.addOnCloseListener(this::finish);
});
}
@@ -317,6 +335,51 @@
}
}
+ /**
+ * Animation callback for different predictive back animation states for the widget picker.
+ */
+ private class BackAnimationCallback implements OnBackAnimationCallback {
+ @Nullable
+ OnBackAnimationCallback mActiveOnBackAnimationCallback;
+
+ @Override
+ public void onBackStarted(@NonNull BackEvent backEvent) {
+ if (mActiveOnBackAnimationCallback != null) {
+ mActiveOnBackAnimationCallback.onBackCancelled();
+ }
+ if (mWidgetSheet != null) {
+ mActiveOnBackAnimationCallback = mWidgetSheet;
+ mActiveOnBackAnimationCallback.onBackStarted(backEvent);
+ }
+ }
+
+ @Override
+ public void onBackInvoked() {
+ if (mActiveOnBackAnimationCallback == null) {
+ return;
+ }
+ mActiveOnBackAnimationCallback.onBackInvoked();
+ mActiveOnBackAnimationCallback = null;
+ }
+
+ @Override
+ public void onBackProgressed(@NonNull BackEvent backEvent) {
+ if (mActiveOnBackAnimationCallback == null) {
+ return;
+ }
+ mActiveOnBackAnimationCallback.onBackProgressed(backEvent);
+ }
+
+ @Override
+ public void onBackCancelled() {
+ if (mActiveOnBackAnimationCallback == null) {
+ return;
+ }
+ mActiveOnBackAnimationCallback.onBackCancelled();
+ mActiveOnBackAnimationCallback = null;
+ }
+ };
+
private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) {
final AppWidgetProviderInfo info = widget.widgetInfo;
if (info == null) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 3fbdc89..0f17a85 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -795,6 +795,11 @@
*/
public void setUIController(@NonNull TaskbarUIController uiController) {
mControllers.setUiController(uiController);
+ if (mControllers.bubbleControllers.isEmpty()) {
+ // if the bubble bar was visible in a previous configuration of taskbar and is being
+ // recreated now without bubbles, clean up any bubble bar adjustments from hotseat
+ bubbleBarVisibilityChanged(/* isVisible= */ false);
+ }
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 4100e51..a3832cd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -394,7 +394,8 @@
BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
// If we're not stashed, we're visible so animate
bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
- mBubbleBarViewController.animateBubbleNotification(bb, /* isExpanding= */ false);
+ mBubbleBarViewController.animateBubbleNotification(
+ bb, /* isExpanding= */ false, /* isUpdate= */ true);
}
if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
// Create the new list
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 7795cfe..fd989b1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -46,7 +46,6 @@
import com.android.launcher3.anim.SpringAnimationBuilder;
import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator;
import com.android.launcher3.util.DisplayController;
-import com.android.wm.shell.Flags;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import java.io.PrintWriter;
@@ -260,10 +259,6 @@
if (!isIconSizeOrPaddingUpdated(newIconSize, newBubbleBarPadding)) {
return;
}
- if (!Flags.animateBubbleSizeChange()) {
- setIconSizeAndPadding(newIconSize, newBubbleBarPadding);
- return;
- }
if (mScalePaddingAnimator != null && mScalePaddingAnimator.isRunning()) {
mScalePaddingAnimator.cancel();
}
@@ -901,6 +896,13 @@
float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
bv.setZ(fullElevationForChild * elevationState);
+ // only update the dot scale if we're expanding or collapsing
+ // TODO b/351904597: update the dot for the first bubble after removal and reorder
+ // since those might happen when the bar is collapsed and will need their dot back
+ if (mWidthAnimator.isRunning()) {
+ bv.setDotScale(widthState);
+ }
+
if (mIsBarExpanded) {
// If bar is on the right, account for bubble bar expanding and shifting left
final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth;
@@ -909,7 +911,6 @@
bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
// When we're expanded, the badge is visible for all bubbles
bv.updateBadgeVisibility(/* show= */ true);
- bv.setDotScale(widthState);
bv.setAlpha(1);
} else {
// If bar is on the right, account for bubble bar expanding and shifting left
@@ -918,7 +919,6 @@
bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
// The badge is always visible for the first bubble
bv.updateBadgeVisibility(/* show= */ i == 0);
- bv.setDotScale(widthState);
// If we're fully collapsed, hide all bubbles except for the first 2. If there are
// only 2 bubbles, hide the second bubble as well because it's the overflow.
if (widthState == 0) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index d7c8a8a..ad81509 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -400,7 +400,7 @@
addedBubble.getView().setOnClickListener(mBubbleClickListener);
mBubbleDragController.setupBubbleView(addedBubble.getView());
if (!suppressAnimation) {
- animateBubbleNotification(addedBubble, isExpanding);
+ animateBubbleNotification(addedBubble, isExpanding, /* isUpdate= */ true);
}
}
@@ -428,18 +428,19 @@
}
return;
}
- animateBubbleNotification(bubble, isExpanding);
+ animateBubbleNotification(bubble, isExpanding, /* isUpdate= */ true);
} else {
Log.w(TAG, "addBubble, bubble was null!");
}
}
/** Animates the bubble bar to notify the user about a bubble change. */
- public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding) {
+ public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding,
+ boolean isUpdate) {
boolean isInApp = mTaskbarStashController.isInApp();
// if this is the first bubble, animate to the initial state. one bubble is the overflow
// so check for at most 2 children.
- if (mBarView.getChildCount() <= 2) {
+ if (mBarView.getChildCount() <= 2 && !isUpdate) {
mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
return;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 0e26c54..4c468bb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -20,6 +20,7 @@
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Outline;
+import android.graphics.Path;
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.AttributeSet;
@@ -47,7 +48,7 @@
private final ImageView mBubbleIcon;
private final ImageView mAppIcon;
- private final int mBubbleSize;
+ private int mBubbleSize;
private float mDragTranslationX;
private float mOffsetX;
@@ -89,8 +90,6 @@
setLayoutDirection(LAYOUT_DIRECTION_LTR);
LayoutInflater.from(context).inflate(R.layout.bubble_view, this);
-
- mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
mBubbleIcon = findViewById(R.id.icon_view);
mAppIcon = findViewById(R.id.app_icon_view);
@@ -107,11 +106,21 @@
}
private void getOutline(Outline outline) {
+ updateBubbleSizeAndDotRender();
final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
final int inset = (mBubbleSize - normalizedSize) / 2;
outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
}
+ private void updateBubbleSizeAndDotRender() {
+ int updatedBubbleSize = Math.min(getWidth(), getHeight());
+ if (updatedBubbleSize == mBubbleSize) return;
+ mBubbleSize = updatedBubbleSize;
+ if (mBubble == null || mBubble instanceof BubbleBarOverflow) return;
+ Path dotPath = ((BubbleBarBubble) mBubble).getDotPath();
+ mDotRenderer = new DotRenderer(mBubbleSize, dotPath, DEFAULT_PATH_SIZE);
+ }
+
/**
* Set translation-x while this bubble is being dragged.
* Translation applied to the view is a sum of {@code translationX} and offset defined by
@@ -141,6 +150,12 @@
applyDragTranslation();
}
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ updateBubbleSizeAndDotRender();
+ }
+
private void applyDragTranslation() {
setTranslationX(mDragTranslationX + mOffsetX);
}
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index b3a9199..1f6c02c 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -33,6 +33,7 @@
import android.text.TextUtils;
import android.util.SparseArray;
+import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.android.launcher3.R;
@@ -48,6 +49,7 @@
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;
@@ -59,7 +61,7 @@
/**
* Manages the caching of task icons and related data.
*/
-public class TaskIconCache implements DisplayInfoChangeListener {
+public class TaskIconCache implements TaskIconDataSource, DisplayInfoChangeListener {
private final Executor mBgExecutor;
@@ -102,7 +104,8 @@
* @param callback The callback to receive the task after its data has been populated.
* @return A cancelable handle to the request
*/
- public CancellableTask getIconInBackground(Task task, GetTaskIconCallback callback) {
+ @Override
+ public CancellableTask getIconInBackground(Task task, @NonNull GetTaskIconCallback callback) {
Preconditions.assertUIThread();
if (task.icon != null) {
// Nothing to load, the icon is already loaded
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 4d6dfc3..f73db5a 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -16,7 +16,8 @@
package com.android.quickstep.recents.data
-import com.android.quickstep.TaskIconCache
+import android.graphics.drawable.Drawable
+import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
import com.android.quickstep.util.GroupTask
import com.android.systemui.shared.recents.model.Task
@@ -38,7 +39,7 @@
class TasksRepository(
private val recentsModel: RecentTasksDataSource,
private val taskThumbnailDataSource: TaskThumbnailDataSource,
- private val taskIconCache: TaskIconCache,
+ private val taskIconDataSource: TaskIconDataSource,
) : RecentTasksRepository {
private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
private val _taskData =
@@ -46,10 +47,19 @@
private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
private val taskData: Flow<List<Task>> =
- combine(_taskData, getThumbnailQueryResults()) { tasks, results ->
+ combine(_taskData, getThumbnailQueryResults(), getIconQueryResults()) {
+ tasks,
+ thumbnailQueryResults,
+ iconQueryResults ->
tasks.forEach { task ->
// Add retrieved thumbnails + remove unnecessary thumbnails
- task.thumbnail = results[task.key.id]
+ task.thumbnail = thumbnailQueryResults[task.key.id]
+
+ // TODO(b/352331675) don't load icons for DesktopTaskView
+ // Add retrieved icons + remove unnecessary icons
+ task.icon = iconQueryResults[task.key.id]?.icon
+ task.titleDescription = iconQueryResults[task.key.id]?.contentDescription
+ task.title = iconQueryResults[task.key.id]?.title
}
tasks
}
@@ -79,7 +89,6 @@
suspendCancellableCoroutine { continuation ->
val cancellableTask =
taskThumbnailDataSource.getThumbnailInBackground(task) {
- task.thumbnail = it
continuation.resume(it)
}
continuation.invokeOnCancellation { cancellableTask?.cancel() }
@@ -109,6 +118,59 @@
}
}
}
+
+ /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
+ private fun getIconDataRequest(task: Task): IconDataRequest =
+ flow {
+ emit(task.key.id to task.getTaskIconQueryResponse())
+ val iconDataResponse: TaskIconQueryResponse? =
+ suspendCancellableCoroutine { continuation ->
+ val cancellableTask =
+ taskIconDataSource.getIconInBackground(task) {
+ icon,
+ contentDescription,
+ title ->
+ continuation.resume(
+ TaskIconQueryResponse(icon, contentDescription, title)
+ )
+ }
+ continuation.invokeOnCancellation { cancellableTask?.cancel() }
+ }
+ emit(task.key.id to iconDataResponse)
+ }
+ .distinctUntilChanged()
+
+ private fun getIconQueryResults(): Flow<Map<Int, TaskIconQueryResponse?>> {
+ val visibleTasks =
+ combine(_taskData, visibleTaskIds) { tasks, visibleIds ->
+ tasks.filter { it.key.id in visibleIds }
+ }
+ val visibleIconDataRequests: Flow<List<IconDataRequest>> =
+ visibleTasks.map { visibleTasksList -> visibleTasksList.map(::getIconDataRequest) }
+ return visibleIconDataRequests.flatMapLatest { iconRequestFlows: List<IconDataRequest> ->
+ if (iconRequestFlows.isEmpty()) {
+ flowOf(emptyMap())
+ } else {
+ combine(iconRequestFlows) { it.toMap() }
+ }
+ }
+ }
}
-typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>
+private data class TaskIconQueryResponse(
+ val icon: Drawable,
+ val contentDescription: String,
+ val title: String
+)
+
+private fun Task.getTaskIconQueryResponse(): TaskIconQueryResponse? {
+ val iconVal = icon ?: return null
+ val titleDescriptionVal = titleDescription ?: return null
+ val titleVal = title ?: return null
+
+ return TaskIconQueryResponse(iconVal, titleDescriptionVal, titleVal)
+}
+
+private typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>
+
+private typealias IconDataRequest = Flow<Pair<Int, TaskIconQueryResponse?>>
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
new file mode 100644
index 0000000..ab699c6
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.data
+
+import com.android.launcher3.util.CancellableTask
+import com.android.quickstep.TaskIconCache.GetTaskIconCallback
+import com.android.systemui.shared.recents.model.Task
+
+interface TaskIconDataSource {
+ fun getIconInBackground(task: Task, callback: GetTaskIconCallback): CancellableTask<*>?
+}
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
index 85238ed..7e51fcf 100644
--- a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
+++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
@@ -50,7 +50,7 @@
private val disappearanceDurationMs: Long,
private val interpolator: Interpolator,
) {
- private val borderAnimationProgress = AnimatedFloat { updateOutline() }
+ private val borderAnimationProgress = AnimatedFloat { _ -> updateOutline() }
private val borderPaint =
Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = borderColor
@@ -224,6 +224,7 @@
val borderWidth: Float
get() = borderWidthPx * animationProgress
+
val alignmentAdjustment: Float
// Outset the border by half the width to create an outwards-growth animation
get() = -borderWidth / 2f + alignmentAdjustmentInset
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index d10bc50..cb8ee06 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -3816,7 +3816,7 @@
anim.setFloat(taskView, taskView.getSecondaryDismissTranslationProperty(),
secondaryTranslation, clampToProgress(LINEAR, animationStartProgress,
dismissTranslationInterpolationEnd));
- anim.setFloat(taskView, TaskView.SCALE_AND_DIM_OUT, 0f,
+ anim.add(taskView.getFocusTransitionScaleAndDimOutAnimator(),
clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT));
} else {
float primaryTranslation =
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 3209fab..004003c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -52,6 +52,7 @@
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.R
import com.android.launcher3.Utilities
+import com.android.launcher3.anim.AnimatedFloat
import com.android.launcher3.config.FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH
import com.android.launcher3.logging.StatsLogManager.LauncherEvent
import com.android.launcher3.model.data.ItemInfo
@@ -420,17 +421,17 @@
focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_FULLSCREEN)
private val focusTransitionScaleAndDim =
focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_SCALE_AND_DIM)
+
/**
- * Variant of [focusTransitionScaleAndDim] that has a built-in interpolator, to be used with
- * [com.android.launcher3.anim.PendingAnimation] via [SCALE_AND_DIM_OUT] only. PendingAnimation
- * doesn't support interpolator per animation, so we'll have to interpolate inside the property.
+ * Returns an animator of [focusTransitionScaleAndDim] that transition out with a built-in
+ * interpolator.
*/
- private var focusTransitionScaleAndDimOut = focusTransitionScaleAndDim.value
- set(value) {
- field = value
- focusTransitionScaleAndDim.value =
- FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(field)
- }
+ fun getFocusTransitionScaleAndDimOutAnimator(): ObjectAnimator =
+ AnimatedFloat { v ->
+ focusTransitionScaleAndDim.value =
+ FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(v)
+ }
+ .animateToValue(1f, 0f)
private var iconAndDimAnimator: ObjectAnimator? = null
// The current background requests to load the task thumbnail and icon
@@ -1615,16 +1616,6 @@
override fun get(taskView: TaskView) = taskView.focusTransitionProgress
}
- @JvmField
- val SCALE_AND_DIM_OUT: FloatProperty<TaskView> =
- object : FloatProperty<TaskView>("scaleAndDimFastOut") {
- override fun setValue(taskView: TaskView, v: Float) {
- taskView.focusTransitionScaleAndDimOut = v
- }
-
- override fun get(taskView: TaskView) = taskView.focusTransitionScaleAndDimOut
- }
-
private val SPLIT_SELECT_TRANSLATION_X: FloatProperty<TaskView> =
object : FloatProperty<TaskView>("splitSelectTranslationX") {
override fun setValue(taskView: TaskView, v: Float) {
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
new file mode 100644
index 0000000..242bc73
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.recents.data
+
+import android.graphics.drawable.Drawable
+import com.android.launcher3.util.CancellableTask
+import com.android.quickstep.TaskIconCache
+import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
+import com.android.systemui.shared.recents.model.Task
+import com.google.common.truth.Truth.assertThat
+import org.mockito.kotlin.mock
+
+class FakeTaskIconDataSource : TaskIconDataSource {
+
+ val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mock() }
+ val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
+ var shouldLoadSynchronously: Boolean = true
+
+ /** 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}"
+ )
+ }
+ if (shouldLoadSynchronously) {
+ wrappedCallback()
+ } else {
+ taskIdToUpdatingTask[task.key.id] = wrappedCallback
+ }
+ return null
+ }
+}
+
+fun Task.assertHasIconDataFromSource(fakeTaskIconDataSource: FakeTaskIconDataSource) {
+ assertThat(icon).isEqualTo(fakeTaskIconDataSource.taskIdToDrawable[key.id])
+ assertThat(titleDescription).isEqualTo("content desc ${key.id}")
+ assertThat(title).isEqualTo("title ${key.id}")
+}
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 c28a85a..88fa190 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
@@ -18,7 +18,6 @@
import android.content.ComponentName
import android.content.Intent
-import com.android.quickstep.TaskIconCache
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
import com.android.systemui.shared.recents.model.Task
@@ -31,7 +30,6 @@
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Test
-import org.mockito.kotlin.mock
@OptIn(ExperimentalCoroutinesApi::class)
class TasksRepositoryTest {
@@ -44,10 +42,10 @@
)
private val recentsModel = FakeRecentTasksDataSource()
private val taskThumbnailDataSource = FakeTaskThumbnailDataSource()
- private val taskIconCache = mock<TaskIconCache>()
+ private val taskIconDataSource = FakeTaskIconDataSource()
private val systemUnderTest =
- TasksRepository(recentsModel, taskThumbnailDataSource, taskIconCache)
+ TasksRepository(recentsModel, taskThumbnailDataSource, taskIconDataSource)
@Test
fun getAllTaskDataReturnsFlattenedListOfTasks() = runTest {
@@ -81,6 +79,22 @@
}
@Test
+ fun setVisibleTasksPopulatesIcons() = runTest {
+ recentsModel.seedTasks(defaultTaskList)
+ systemUnderTest.getAllTaskData(forceRefresh = true)
+
+ systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+ // .drop(1) to ignore initial null content before from thumbnail was loaded.
+ systemUnderTest
+ .getTaskDataById(1)
+ .drop(1)
+ .first()!!
+ .assertHasIconDataFromSource(taskIconDataSource)
+ systemUnderTest.getTaskDataById(2).first()!!.assertHasIconDataFromSource(taskIconDataSource)
+ }
+
+ @Test
fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = runTest {
recentsModel.seedTasks(defaultTaskList)
val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
@@ -101,7 +115,28 @@
}
@Test
- fun retrievedThumbnailsAreDiscardedWhenTaskBecomesInvisible() = runTest {
+ fun changingVisibleTasksContainsAlreadyPopulatedIcons() = runTest {
+ recentsModel.seedTasks(defaultTaskList)
+ systemUnderTest.getAllTaskData(forceRefresh = true)
+
+ systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+ // .drop(1) to ignore initial null content before from icon was loaded.
+ systemUnderTest
+ .getTaskDataById(2)
+ .drop(1)
+ .first()!!
+ .assertHasIconDataFromSource(taskIconDataSource)
+
+ // Prevent new loading of Drawables
+ taskThumbnailDataSource.shouldLoadSynchronously = false
+ systemUnderTest.setVisibleTasks(listOf(2, 3))
+
+ systemUnderTest.getTaskDataById(2).first()!!.assertHasIconDataFromSource(taskIconDataSource)
+ }
+
+ @Test
+ fun retrievedImagesAreDiscardedWhenTaskBecomesInvisible() = runTest {
recentsModel.seedTasks(defaultTaskList)
val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
systemUnderTest.getAllTaskData(forceRefresh = true)
@@ -109,14 +144,20 @@
systemUnderTest.setVisibleTasks(listOf(1, 2))
// .drop(1) to ignore initial null content before from thumbnail was loaded.
- assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail)
- .isEqualTo(bitmap2)
+ val task2 = systemUnderTest.getTaskDataById(2).drop(1).first()!!
+ assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2)
+ task2.assertHasIconDataFromSource(taskIconDataSource)
// Prevent new loading of Bitmaps
taskThumbnailDataSource.shouldLoadSynchronously = false
+ taskIconDataSource.shouldLoadSynchronously = false
systemUnderTest.setVisibleTasks(listOf(0, 1))
- assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull()
+ val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!!
+ assertThat(task2AfterVisibleTasksChanged.thumbnail).isNull()
+ assertThat(task2AfterVisibleTasksChanged.icon).isNull()
+ assertThat(task2AfterVisibleTasksChanged.titleDescription).isNull()
+ assertThat(task2AfterVisibleTasksChanged.title).isNull()
}
@Test
diff --git a/res/anim-v33/shared_x_axis_activity_close_enter.xml b/res/anim-v33/shared_x_axis_activity_close_enter.xml
deleted file mode 100644
index 3d7ad2b..0000000
--- a/res/anim-v33/shared_x_axis_activity_close_enter.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 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.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:shareInterpolator="false"
- android:showBackdrop="true">
-
- <alpha
- android:fromAlpha="0.0"
- android:toAlpha="1.0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/standard_decelerate_interpolator"
- android:startOffset="100"
- android:duration="350" />
-
- <translate
- android:fromXDelta="-25%"
- android:toXDelta="0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/emphasized_interpolator"
- android:startOffset="0"
- android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/anim-v33/shared_x_axis_activity_close_exit.xml b/res/anim-v33/shared_x_axis_activity_close_exit.xml
deleted file mode 100644
index fb63602..0000000
--- a/res/anim-v33/shared_x_axis_activity_close_exit.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 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.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:shareInterpolator="false">
-
- <alpha
- android:fromAlpha="1.0"
- android:toAlpha="0.0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/standard_accelerate_interpolator"
- android:startOffset="0"
- android:duration="100" />
-
- <translate
- android:fromXDelta="0"
- android:toXDelta="25%"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/emphasized_interpolator"
- android:startOffset="0"
- android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/anim-v33/shared_x_axis_activity_open_enter.xml b/res/anim-v33/shared_x_axis_activity_open_enter.xml
deleted file mode 100644
index cba74ba..0000000
--- a/res/anim-v33/shared_x_axis_activity_open_enter.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 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.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:shareInterpolator="false"
- android:showBackdrop="true">
-
- <alpha
- android:fromAlpha="0.0"
- android:toAlpha="1.0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/standard_decelerate_interpolator"
- android:startOffset="100"
- android:duration="350" />
-
- <translate
- android:fromXDelta="25%"
- android:toXDelta="0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/emphasized_interpolator"
- android:startOffset="0"
- android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/anim-v33/shared_x_axis_activity_open_exit.xml b/res/anim-v33/shared_x_axis_activity_open_exit.xml
deleted file mode 100644
index 22e878d..0000000
--- a/res/anim-v33/shared_x_axis_activity_open_exit.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 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.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:shareInterpolator="false">
-
- <alpha
- android:fromAlpha="1.0"
- android:toAlpha="0.0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/standard_accelerate_interpolator"
- android:startOffset="0"
- android:duration="100" />
-
- <translate
- android:fromXDelta="0"
- android:toXDelta="-25%"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/emphasized_interpolator"
- android:startOffset="0"
- android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/values-v33/style.xml b/res/values-v33/style.xml
deleted file mode 100644
index 1261b23..0000000
--- a/res/values-v33/style.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* Copyright (C) 2022 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.
-*/
--->
-
-<resources>
- <style name="HomeSettings.Theme" parent="@android:style/Theme.DeviceDefault.Settings">
- <item name="android:listPreferredItemPaddingEnd">16dp</item>
- <item name="android:listPreferredItemPaddingStart">24dp</item>
- <item name="android:navigationBarColor">@android:color/transparent</item>
- <item name="android:statusBarColor">@android:color/transparent</item>
- <item name="android:switchStyle">@style/SwitchStyle</item>
- <item name="android:textAppearanceListItem">@style/HomeSettings.PreferenceTitle</item>
- <item name="android:windowActionBar">false</item>
- <item name="android:windowNoTitle">true</item>
- <item name="preferenceTheme">@style/HomeSettings.PreferenceTheme</item>
- <item name="android:windowAnimationStyle">@style/Animation.SharedBackground</item>
- </style>
-
- <style name="Animation.SharedBackground" parent="@android:style/Animation.Activity">
- <item name="android:activityOpenEnterAnimation">@anim/shared_x_axis_activity_open_enter</item>
- <item name="android:activityOpenExitAnimation">@anim/shared_x_axis_activity_open_exit</item>
- <item name="android:activityCloseEnterAnimation">@anim/shared_x_axis_activity_close_enter</item>
- <item name="android:activityCloseExitAnimation">@anim/shared_x_axis_activity_close_exit</item>
- </style>
-</resources>
\ No newline at end of file
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index b51e850..ef56246 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -37,6 +37,8 @@
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.CellPosMapper.CellPos;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.debug.TestEvent;
+import com.android.launcher3.debug.TestEventEmitter;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.logging.InstanceId;
@@ -221,6 +223,9 @@
dl.addView(frame);
frame.mIsOpen = true;
frame.post(() -> frame.snapToWidget(false));
+ TestEventEmitter.INSTANCE.get(widget.getContext()).sendEvent(
+ TestEvent.RESIZE_FRAME_SHOWING
+ );
}
private void setCornerRadiusFromWidget() {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5c052b2..cb897dc 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -166,7 +166,6 @@
import androidx.core.os.BuildCompat;
import androidx.window.embedding.RuleController;
-import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.allapps.ActivityAllAppsContainerView;
@@ -181,6 +180,8 @@
import com.android.launcher3.celllayout.CellPosMapper.TwoPanelCellPosMapper;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.debug.TestEvent;
+import com.android.launcher3.debug.TestEventEmitter;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
@@ -595,6 +596,7 @@
RuleController.getInstance(this).setRules(
RuleController.parseRules(this, R.xml.split_configuration));
}
+ TestEventEmitter.INSTANCE.get(this).sendEvent(TestEvent.LAUNCHER_ON_CREATE);
}
protected ModelCallbacks createModelCallbacks() {
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 13062b6..83c34ce 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -11,6 +11,8 @@
import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID
import com.android.launcher3.allapps.AllAppsStore
import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.debug.TestEvent
+import com.android.launcher3.debug.TestEventEmitter
import com.android.launcher3.model.BgDataModel
import com.android.launcher3.model.StringCache
import com.android.launcher3.model.data.AppInfo
@@ -156,6 +158,7 @@
/*pause=*/ false,
deviceProfile.isTwoPanels
)
+ TestEventEmitter.INSTANCE.get(launcher).sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
}
/**
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index e601a3e..2995e8a 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -80,6 +80,8 @@
import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.celllayout.CellPosMapper.CellPos;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.debug.TestEvent;
+import com.android.launcher3.debug.TestEventEmitter;
import com.android.launcher3.dot.FolderDotInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
@@ -314,7 +316,6 @@
*/
public Workspace(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
-
mLauncher = Launcher.getLauncher(context);
mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this);
mWallpaperManager = WallpaperManager.getInstance(context);
@@ -2218,6 +2219,7 @@
if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
d.stateAnnouncer.completeAction(R.string.item_moved);
}
+ TestEventEmitter.INSTANCE.get(getContext()).sendEvent(TestEvent.WORKSPACE_ON_DROP);
}
@Nullable
diff --git a/src/com/android/launcher3/anim/AnimatedFloat.java b/src/com/android/launcher3/anim/AnimatedFloat.java
index b414ab6..4441164 100644
--- a/src/com/android/launcher3/anim/AnimatedFloat.java
+++ b/src/com/android/launcher3/anim/AnimatedFloat.java
@@ -20,6 +20,8 @@
import android.animation.ObjectAnimator;
import android.util.FloatProperty;
+import java.util.function.Consumer;
+
/**
* A mutable float which allows animating the value
*/
@@ -38,9 +40,9 @@
}
};
- private static final Runnable NO_OP = () -> { };
+ private static final Consumer<Float> NO_OP = t -> { };
- private final Runnable mUpdateCallback;
+ private final Consumer<Float> mUpdateCallback;
private ObjectAnimator mValueAnimator;
// Only non-null when an animation is playing to this value.
private Float mEndValue;
@@ -52,6 +54,10 @@
}
public AnimatedFloat(Runnable updateCallback) {
+ this(v -> updateCallback.run());
+ }
+
+ public AnimatedFloat(Consumer<Float> updateCallback) {
mUpdateCallback = updateCallback;
}
@@ -60,6 +66,11 @@
value = initialValue;
}
+ public AnimatedFloat(Consumer<Float> updateCallback, float initialValue) {
+ this(updateCallback);
+ value = initialValue;
+ }
+
/**
* Returns an animation from the current value to the given value.
*/
@@ -99,7 +110,7 @@
public void updateValue(float v) {
if (Float.compare(v, value) != 0) {
value = v;
- mUpdateCallback.run();
+ mUpdateCallback.accept(value);
}
}
diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java
index e58890f..47a2bdd 100644
--- a/src/com/android/launcher3/anim/PendingAnimation.java
+++ b/src/com/android/launcher3/anim/PendingAnimation.java
@@ -59,6 +59,13 @@
add(anim, springProperty);
}
+ /**
+ * Utility method to sent an interpolator on an animation and add it to the list
+ */
+ public void add(Animator anim, TimeInterpolator interpolator) {
+ add(anim, interpolator, SpringProperty.DEFAULT);
+ }
+
@Override
public void add(Animator anim) {
add(anim, SpringProperty.DEFAULT);
diff --git a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
new file mode 100644
index 0000000..650df5a
--- /dev/null
+++ b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.debug
+
+import android.content.Context
+import com.android.launcher3.util.MainThreadInitializedObject
+import com.android.launcher3.util.SafeCloseable
+
+/** Events fired by the launcher. */
+enum class TestEvent(val event: String) {
+ LAUNCHER_ON_CREATE("LAUNCHER_ON_CREATE"),
+ WORKSPACE_ON_DROP("WORKSPACE_ON_DROP"),
+ RESIZE_FRAME_SHOWING("RESIZE_FRAME_SHOWING"),
+ WORKSPACE_FINISH_LOADING("WORKSPACE_FINISH_LOADING"),
+}
+
+/** Interface to create TestEventEmitters. */
+interface TestEventEmitter : SafeCloseable {
+
+ companion object {
+ @JvmField
+ val INSTANCE =
+ MainThreadInitializedObject<TestEventEmitter> { _: Context? ->
+ TestEventsEmitterProduction()
+ }
+ }
+
+ fun sendEvent(event: TestEvent)
+}
+
+/**
+ * TestEventsEmitterProduction shouldn't do anything since it runs on the launcher code and not on
+ * tests. This is just a placeholder and test should override this class.
+ */
+class TestEventsEmitterProduction : TestEventEmitter {
+
+ override fun close() {}
+
+ override fun sendEvent(event: TestEvent) {}
+}
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index bc5a164..c50c008 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -27,6 +27,7 @@
import android.view.View;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.app.animation.Interpolators;
import com.android.launcher3.DragSource;
@@ -69,8 +70,9 @@
*/
protected DragDriver mDragDriver = null;
+ @VisibleForTesting
/** Options controlling the drag behavior. */
- protected DragOptions mOptions;
+ public DragOptions mOptions;
/** Coordinate for motion down event */
protected final Point mMotionDown = new Point();
@@ -79,7 +81,8 @@
protected final Point mTmpPoint = new Point();
- protected DropTarget.DragObject mDragObject;
+ @VisibleForTesting
+ public DropTarget.DragObject mDragObject;
/** Who can receive drop events */
private final ArrayList<DropTarget> mDropTargets = new ArrayList<>();
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index ae8f1d5..6088941 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -508,6 +508,7 @@
&& !SHOULD_SHOW_FIRST_PAGE_WIDGET) {
CellLayout firstScreen = mWorkspaceScreens.get(FIRST_SCREEN_ID);
View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen, false);
+ // TODO: set bgHandler on qsb when it is BaseTemplateCard, which requires API changes.
CellLayoutLayoutParams lp = new CellLayoutLayoutParams(
0, 0, firstScreen.getCountX(), 1);
lp.canReorder = false;
diff --git a/tests/assets/ReorderWidgets/full_reorder_case b/tests/assets/ReorderWidgets/full_reorder_case
index 850e4fd..2890b79 100644
--- a/tests/assets/ReorderWidgets/full_reorder_case
+++ b/tests/assets/ReorderWidgets/full_reorder_case
@@ -17,12 +17,12 @@
# Test 4x4
board: 4x4
xxxx
-bbmm
+bbaa
iimm
-iiaa
+iimm
arguments: 0 2
board: 4x4
xxxx
-bbii
+bbaa
mmii
-mmaa
\ No newline at end of file
+mmii
\ No newline at end of file
diff --git a/tests/assets/ReorderWidgets/push_reorder_case b/tests/assets/ReorderWidgets/push_reorder_case
index 8e845a2..1eacfae 100644
--- a/tests/assets/ReorderWidgets/push_reorder_case
+++ b/tests/assets/ReorderWidgets/push_reorder_case
@@ -17,28 +17,28 @@
#Test 5x5
board: 5x5
xxxxx
-bbbm-
+bbb--
--ccc
--ddd
------
-arguments: 2 1
+----m
+arguments: 2 2
board: 5x5
xxxxx
---m--
bbb--
+--m--
--ccc
--ddd
#6x5 Test
board: 6x5
xxxxxx
-bbbbm-
+bbbb--
--aaa-
--ddd-
-------
-arguments: 2 1
+-----m
+arguments: 2 2
board: 6x5
xxxxxx
---m---
bbbb--
+--m---
--aaa-
--ddd-
\ No newline at end of file
diff --git a/tests/assets/ReorderWidgets/simple_reorder_case b/tests/assets/ReorderWidgets/simple_reorder_case
index 2c50ce4..991ccb5 100644
--- a/tests/assets/ReorderWidgets/simple_reorder_case
+++ b/tests/assets/ReorderWidgets/simple_reorder_case
@@ -21,7 +21,7 @@
--mm-
-----
-----
-arguments: 0 4
+arguments: 0 3
board: 5x5
xxxxx
-----
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
index 419cb3d..f1403e5 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
@@ -54,7 +54,7 @@
}
public static class Arguments extends TestSection {
- String[] arguments;
+ public String[] arguments;
public Arguments(String[] arguments) {
super(State.ARGUMENTS);
diff --git a/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java b/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
deleted file mode 100644
index 28a1325..0000000
--- a/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
- * Copyright (C) 2022 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.celllayout;
-
-import static android.platform.uiautomator_helpers.DeviceHelpers.getContext;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.graphics.Point;
-import android.net.Uri;
-import android.util.Log;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.MultipageCellLayout;
-import com.android.launcher3.celllayout.board.CellLayoutBoard;
-import com.android.launcher3.celllayout.board.TestWorkspaceBuilder;
-import com.android.launcher3.celllayout.board.WidgetRect;
-import com.android.launcher3.tapl.Widget;
-import com.android.launcher3.tapl.WidgetResizeFrame;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
-import com.android.launcher3.util.ModelTestExtensions;
-import com.android.launcher3.util.rule.ShellCommandRule;
-
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Assume;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TaplReorderWidgetsTest extends AbstractLauncherUiTest<Launcher> {
-
- @Rule
- public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
-
- private static final String TAG = TaplReorderWidgetsTest.class.getSimpleName();
-
- private static final List<String> FOLDABLE_GRIDS = List.of("normal", "practical", "reasonable");
-
- TestWorkspaceBuilder mWorkspaceBuilder;
-
- @Before
- public void setup() throws Throwable {
- mWorkspaceBuilder = new TestWorkspaceBuilder(mTargetContext);
- super.setUp();
- }
-
- @After
- public void tearDown() {
- ModelTestExtensions.INSTANCE.clearModelDb(
- LauncherAppState.getInstance(getContext()).getModel()
- );
- }
-
- /**
- * Validate if the given board represent the current CellLayout
- **/
- private boolean validateBoard(List<CellLayoutBoard> testBoards) {
- ArrayList<CellLayoutBoard> workspaceBoards = workspaceToBoards();
- if (workspaceBoards.size() < testBoards.size()) {
- return false;
- }
- for (int i = 0; i < testBoards.size(); i++) {
- if (testBoards.get(i).compareTo(workspaceBoards.get(i)) != 0) {
- return false;
- }
- }
- return true;
- }
-
- private FavoriteItemsTransaction buildWorkspaceFromBoards(List<CellLayoutBoard> boards,
- FavoriteItemsTransaction transaction) {
- for (int i = 0; i < boards.size(); i++) {
- CellLayoutBoard board = boards.get(i);
- mWorkspaceBuilder.buildFromBoard(board, transaction, i);
- }
- return transaction;
- }
-
- private void printCurrentWorkspace() {
- InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
- ArrayList<CellLayoutBoard> boards = workspaceToBoards();
- for (int i = 0; i < boards.size(); i++) {
- Log.d(TAG, "Screen number " + i);
- Log.d(TAG, ".\n" + boards.get(i).toString(idp.numColumns, idp.numRows));
- }
- }
-
- private ArrayList<CellLayoutBoard> workspaceToBoards() {
- return getFromLauncher(CellLayoutTestUtils::workspaceToBoards);
- }
-
- private WidgetRect getWidgetClosestTo(Point point) {
- ArrayList<CellLayoutBoard> workspaceBoards = workspaceToBoards();
- int maxDistance = 9999;
- WidgetRect bestRect = null;
- for (int i = 0; i < workspaceBoards.get(0).getWidgets().size(); i++) {
- WidgetRect widget = workspaceBoards.get(0).getWidgets().get(i);
- if (widget.getCellX() == 0 && widget.getCellY() == 0) {
- continue;
- }
- int distance = Math.abs(point.x - widget.getCellX())
- + Math.abs(point.y - widget.getCellY());
- if (distance == 0) {
- break;
- }
- if (distance < maxDistance) {
- maxDistance = distance;
- bestRect = widget;
- }
- }
- return bestRect;
- }
-
- /**
- * This function might be odd, its function is to select a widget and leave it in its place.
- * The idea is to make the test broader and also test after a widgets resized because the
- * underlying code does different things in that case
- */
- private void triggerWidgetResize(ReorderTestCase testCase) {
- WidgetRect widgetRect = getWidgetClosestTo(testCase.moveMainTo);
- if (widgetRect == null) {
- // Some test doesn't have a widget in the final position, in those cases we will ignore
- // them
- return;
- }
- Widget widget = mLauncher.getWorkspace().getWidgetAtCell(widgetRect.getCellX(),
- widgetRect.getCellY());
- WidgetResizeFrame resizeFrame = widget.dragWidgetToWorkspace(widgetRect.getCellX(),
- widgetRect.getCellY(), widgetRect.getSpanX(), widgetRect.getSpanY());
- resizeFrame.dismiss();
- }
-
- private void runTestCase(ReorderTestCase testCase) {
- WidgetRect mainWidgetCellPos = CellLayoutBoard.getMainFromList(
- testCase.mStart);
-
- FavoriteItemsTransaction transaction =
- new FavoriteItemsTransaction(mTargetContext);
- transaction = buildWorkspaceFromBoards(testCase.mStart, transaction);
- transaction.commit();
- mLauncher.waitForLauncherInitialized();
- // resetLoaderState triggers the launcher to start loading the workspace which allows
- // waitForLauncherCondition to wait for that condition, otherwise the condition would
- // always be true and it wouldn't wait for the changes to be applied.
- waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
-
- triggerWidgetResize(testCase);
-
- Widget widget = mLauncher.getWorkspace().getWidgetAtCell(mainWidgetCellPos.getCellX(),
- mainWidgetCellPos.getCellY());
- assertNotNull(widget);
- WidgetResizeFrame resizeFrame = widget.dragWidgetToWorkspace(testCase.moveMainTo.x,
- testCase.moveMainTo.y, mainWidgetCellPos.getSpanX(), mainWidgetCellPos.getSpanY());
- resizeFrame.dismiss();
-
- boolean isValid = false;
- for (List<CellLayoutBoard> boards : testCase.mEnd) {
- isValid |= validateBoard(boards);
- if (isValid) break;
- }
- printCurrentWorkspace();
- assertTrue("Non of the valid boards match with the current state", isValid);
- }
-
- /**
- * Run only the test define for the current grid size if such test exist
- *
- * @param testCaseMap map containing all the tests per grid size (Point)
- */
- private boolean runTestCaseMap(Map<Point, ReorderTestCase> testCaseMap, String testName) {
- Point iconGridDimensions = mLauncher.getWorkspace().getIconGridDimensions();
- Log.d(TAG, "Running test " + testName + " for grid " + iconGridDimensions);
- if (!testCaseMap.containsKey(iconGridDimensions)) {
- Log.d(TAG, "The test " + testName + " doesn't support " + iconGridDimensions
- + " grid layout");
- return false;
- }
- runTestCase(testCaseMap.get(iconGridDimensions));
-
- return true;
- }
-
- private void runTestCaseMapForAllGrids(Map<Point, ReorderTestCase> testCaseMap,
- String testName) {
- boolean runAtLeastOnce = false;
- for (String grid : FOLDABLE_GRIDS) {
- applyGridOption(grid);
- mLauncher.waitForLauncherInitialized();
- runAtLeastOnce |= runTestCaseMap(testCaseMap, testName);
- }
- Assume.assumeTrue("None of the grids are supported", runAtLeastOnce);
- }
-
- private void applyGridOption(Object argValue) {
- String testProviderAuthority = mTargetContext.getPackageName() + ".grid_control";
- Uri gridUri = new Uri.Builder()
- .scheme(ContentResolver.SCHEME_CONTENT)
- .authority(testProviderAuthority)
- .appendPath("default_grid")
- .build();
- ContentValues values = new ContentValues();
- values.putObject("name", argValue);
- Assert.assertEquals(1,
- mTargetContext.getContentResolver().update(gridUri, values, null, null));
- }
-
- @Test
- public void simpleReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/simple_reorder_case"),
- "push_reorder_case");
- }
-
- @Test
- public void pushTest() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/push_reorder_case"),
- "push_reorder_case");
- }
-
- @Test
- public void fullReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/full_reorder_case"),
- "full_reorder_case");
- }
-
- @Test
- public void moveOutReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/move_out_reorder_case"),
- "move_out_reorder_case");
- }
-
- @Test
- public void multipleCellLayoutsSimpleReorder() throws Exception {
- Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
- l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
- runTestCaseMapForAllGrids(getTestMap("ReorderWidgets/multiple_cell_layouts_simple_reorder"),
- "multiple_cell_layouts_simple_reorder");
- }
-
- @Test
- public void multipleCellLayoutsNoSpaceReorder() throws Exception {
- Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
- l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
- runTestCaseMapForAllGrids(
- getTestMap("ReorderWidgets/multiple_cell_layouts_no_space_reorder"),
- "multiple_cell_layouts_no_space_reorder");
- }
-
- @Test
- public void multipleCellLayoutsReorderToOtherSide() throws Exception {
- Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
- l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
- runTestCaseMapForAllGrids(
- getTestMap("ReorderWidgets/multiple_cell_layouts_reorder_other_side"),
- "multiple_cell_layouts_reorder_other_side");
- }
-
- private void addTestCase(Iterator<CellLayoutTestCaseReader.TestSection> sections,
- Map<Point, ReorderTestCase> testCaseMap) {
- CellLayoutTestCaseReader.Board startBoard =
- ((CellLayoutTestCaseReader.Board) sections.next());
- CellLayoutTestCaseReader.Arguments point =
- ((CellLayoutTestCaseReader.Arguments) sections.next());
- CellLayoutTestCaseReader.Board endBoard =
- ((CellLayoutTestCaseReader.Board) sections.next());
- Point moveTo = new Point(Integer.parseInt(point.arguments[0]),
- Integer.parseInt(point.arguments[1]));
- testCaseMap.put(endBoard.gridSize,
- new ReorderTestCase(startBoard.board, moveTo, endBoard.board));
- }
-
- private Map<Point, ReorderTestCase> getTestMap(String testPath) throws IOException {
- Map<Point, ReorderTestCase> testCaseMap = new HashMap<>();
- Iterator<CellLayoutTestCaseReader.TestSection> iterableSection =
- CellLayoutTestCaseReader.readFromFile(testPath).parse().iterator();
- while (iterableSection.hasNext()) {
- addTestCase(iterableSection, testCaseMap);
- }
- return testCaseMap;
- }
-}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
new file mode 100644
index 0000000..4cecb5a
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.celllayout.integrationtest
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import com.android.launcher3.CellLayout
+import com.android.launcher3.Workspace
+import com.android.launcher3.util.CellAndSpan
+import com.android.launcher3.widget.LauncherAppWidgetHostView
+
+object TestUtils {
+ fun <T> searchChildren(viewGroup: ViewGroup, type: Class<T>): T? where T : View {
+ for (i in 0..<viewGroup.childCount) {
+ val child = viewGroup.getChildAt(i)
+ if (type.isInstance(child)) {
+ return type.cast(child)
+ }
+ if (child is ViewGroup) {
+ val result = searchChildren(child, type)
+ if (result != null) {
+ return result
+ }
+ }
+ }
+ return null
+ }
+
+ fun getWidgetAtCell(
+ workspace: Workspace<*>,
+ cellX: Int,
+ cellY: Int
+ ): LauncherAppWidgetHostView {
+ val view =
+ (workspace.getPageAt(workspace.currentPage) as CellLayout).getChildAt(cellX, cellY)
+ assert(view != null) { "There is no view at $cellX , $cellY" }
+ assert(view is LauncherAppWidgetHostView) { "The view at $cellX , $cellY is not a widget" }
+ return view as LauncherAppWidgetHostView
+ }
+
+ fun getCellTopLeftRelativeToCellLayout(
+ workspace: Workspace<*>,
+ cellAndSpan: CellAndSpan
+ ): Point {
+ val target = Rect()
+ val cellLayout = workspace.getPageAt(workspace.currentPage) as CellLayout
+ cellLayout.cellToRect(
+ cellAndSpan.cellX,
+ cellAndSpan.cellY,
+ cellAndSpan.spanX,
+ cellAndSpan.spanY,
+ target
+ )
+ return Point(target.left, target.top)
+ }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
new file mode 100644
index 0000000..fb61ced
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.celllayout.integrationtest.events
+
+import android.content.Context
+import com.android.launcher3.debug.TestEvent
+import com.android.launcher3.debug.TestEventEmitter
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Rule to create EventWaiters to wait for events that happens on the Launcher. For reference look
+ * at [TestEvent] for existing events.
+ *
+ * Waiting for event should be used to prevent race conditions, it provides a more precise way of
+ * waiting for events compared to [AbstractLauncherUiTest#waitForLauncherCondition].
+ *
+ * This class overrides the [TestEventEmitter] with [TestEventsEmitterImplementation] and makes sure
+ * to return the [TestEventEmitter] to the previous value when finished.
+ */
+class EventsRule(val context: Context) : TestRule {
+
+ private var prevEventEmitter: TestEventEmitter? = null
+
+ private val eventEmitter = TestEventsEmitterImplementation()
+
+ override fun apply(base: Statement, description: Description?): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ beforeTest()
+ base.evaluate()
+ afterTest()
+ }
+ }
+ }
+
+ fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
+ return eventEmitter.createEventWaiter(expectedEvent)
+ }
+
+ private fun beforeTest() {
+ prevEventEmitter = TestEventEmitter.INSTANCE.get(context)
+ TestEventEmitter.INSTANCE.initializeForTesting(eventEmitter)
+ }
+
+ private fun afterTest() {
+ TestEventEmitter.INSTANCE.initializeForTesting(prevEventEmitter)
+ }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
new file mode 100644
index 0000000..365ad4b
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.celllayout.integrationtest.events
+
+import android.util.Log
+import com.android.launcher3.debug.TestEvent
+import com.android.launcher3.debug.TestEventEmitter
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeoutOrNull
+
+enum class EventStatus() {
+ SUCCESS,
+ FAILURE,
+ TIMEOUT,
+}
+
+class EventWaiter(val eventToWait: TestEvent) {
+ private val deferrable = CompletableDeferred<EventStatus>()
+
+ companion object {
+ private const val TAG = "EventWaiter"
+ }
+
+ fun waitForSignal(timeout: Long = TimeUnit.SECONDS.toMillis(10)) = runBlocking {
+ var status = withTimeoutOrNull(timeout) { deferrable.await() }
+ if (status == null) {
+ status = EventStatus.TIMEOUT
+ }
+ if (status != EventStatus.SUCCESS) {
+ throw Exception("Failure waiting for event $eventToWait, failure = $status")
+ }
+ }
+
+ fun terminate() {
+ deferrable.complete(EventStatus.SUCCESS)
+ }
+}
+
+class TestEventsEmitterImplementation() : TestEventEmitter {
+ companion object {
+ private const val TAG = "TestEvents"
+ }
+
+ private val expectedEvents: ArrayDeque<EventWaiter> = ArrayDeque()
+
+ fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
+ val eventWaiter = EventWaiter(expectedEvent)
+ expectedEvents.add(eventWaiter)
+ return eventWaiter
+ }
+
+ private fun clearQueue() {
+ expectedEvents.clear()
+ }
+
+ override fun sendEvent(event: TestEvent) {
+ Log.d(TAG, "Signal received $event")
+ Log.d(TAG, "Total expected events ${expectedEvents.size}")
+ if (expectedEvents.isEmpty()) return
+ val eventWaiter = expectedEvents.last()
+ if (eventWaiter.eventToWait == event) {
+ Log.d(TAG, "Removing $event")
+ expectedEvents.removeLast()
+ eventWaiter.terminate()
+ } else {
+ Log.d(TAG, "Not matching $event")
+ }
+ }
+
+ override fun close() {
+ clearQueue()
+ }
+}
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index ae24a57..9e4299e 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -113,8 +113,6 @@
@Test
@PortraitLandscape
- @ScreenRecordRule.ScreenRecord // b/329935119
- @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/329935119
public void testSinglePageDragIconWhenMultiplePageScrollingIsPossible() {
Workspace workspace = mLauncher.getWorkspace();