Merge "Fix Taskbar not present in Desktop Mode after unlocking" into main
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 3aac1b6..4abfbbe 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -29,8 +29,8 @@
launcher:hoverBorderColor="@color/materialColorPrimary">
<ViewStub
- android:id="@+id/snapshot"
- android:inflatedId="@id/snapshot"
+ android:id="@+id/task_content_view"
+ android:inflatedId="@id/task_content_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
diff --git a/quickstep/res/layout/task_content_view.xml b/quickstep/res/layout/task_content_view.xml
new file mode 100644
index 0000000..9055ccd
--- /dev/null
+++ b/quickstep/res/layout/task_content_view.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ -->
+<com.android.quickstep.task.thumbnail.TaskContentView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/task_content_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" />
\ No newline at end of file
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index 3e6f5ed..a7c4856 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -34,14 +34,14 @@
launcher:hoverBorderColor="@color/materialColorPrimary">
<ViewStub
- android:id="@+id/snapshot"
- android:inflatedId="@id/snapshot"
+ android:id="@+id/task_content_view"
+ android:inflatedId="@id/task_content_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ViewStub
- android:id="@+id/bottomright_snapshot"
- android:inflatedId="@id/bottomright_snapshot"
+ android:id="@+id/bottomright_task_content_view"
+ android:inflatedId="@id/bottomright_task_content_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
diff --git a/quickstep/res/layout/task_thumbnail_view_header.xml b/quickstep/res/layout/task_header_view.xml
similarity index 78%
rename from quickstep/res/layout/task_thumbnail_view_header.xml
rename to quickstep/res/layout/task_header_view.xml
index 70e4a42..ea5c24e 100644
--- a/quickstep/res/layout/task_thumbnail_view_header.xml
+++ b/quickstep/res/layout/task_header_view.xml
@@ -13,12 +13,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.quickstep.views.TaskThumbnailViewHeader
- xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.quickstep.views.TaskHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:id="@+id/task_thumbnail_view_header"
+ android:id="@+id/task_header_view"
android:background="@drawable/task_thumbnail_header_bg">
<androidx.constraintlayout.widget.ConstraintLayout
@@ -38,22 +37,17 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/header_app_title"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintHorizontal_bias="0"
- app:layout_constraintVertical_bias="0.5"
- app:layout_constraintHorizontal_chainStyle="spread_inside" />
+ app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/header_app_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/task_thumbnail_header_margin_between_views"
- android:layout_marginEnd="@dimen/task_thumbnail_header_margin_between_views"
+ android:maxLines="1"
android:text="@string/header_default_app_title"
- app:layout_constraintStart_toEndOf="@id/header_app_icon"
- app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintHorizontal_bias="0"
- app:layout_constraintVertical_bias="0.5" />
+ app:layout_constraintStart_toEndOf="@id/header_app_icon"
+ app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/header_close_button"
android:contentDescription="@string/header_close_icon_description"
@@ -66,7 +60,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintHorizontal_bias="1"
- app:layout_constraintVertical_bias="0.5" />
+ app:layout_constraintStart_toEndOf="@id/header_app_title"
+ app:layout_constraintHorizontal_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
-</com.android.quickstep.views.TaskThumbnailViewHeader>
+</com.android.quickstep.views.TaskHeaderView>
diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml
index 3b96615..8280e13 100644
--- a/quickstep/res/layout/task_thumbnail.xml
+++ b/quickstep/res/layout/task_thumbnail.xml
@@ -17,7 +17,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/snapshot"
android:layout_width="match_parent"
- android:layout_height="match_parent" >
+ android:layout_height="0dp"
+ android:layout_weight="1" >
<com.android.quickstep.views.FixedSizeImageView
android:id="@+id/task_thumbnail"
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
index 688018b..1438edf 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
@@ -64,18 +64,13 @@
fun createAnimators(info: TransitionInfo, finishCallback: (Animator) -> Unit): List<Animator> {
val launchChange = getLaunchChange(info)
- requireNotNull(launchChange) {
- val changesString =
+ if (launchChange == null) {
+ val tasksInfo =
info.changes.joinToString(", ") { change ->
- "Change: mode=${change.mode}, " +
- "taskId=${change.taskInfo?.id}, " +
- "isFreeform=${change.taskInfo?.isFreeform}"
+ "${change.taskInfo?.taskId}:${change.taskInfo?.isFreeform}"
}
- Log.e(
- TAG,
- "No launch change found: Transition type=${info.type}, changes=$changesString",
- )
- "expected an app launch Change"
+ Log.e(TAG, "No launch change found: Transition info=$info, tasks state=$tasksInfo")
+ return emptyList()
}
val transaction = transactionSupplier.get()
@@ -105,10 +100,14 @@
}
private fun getLaunchChange(info: TransitionInfo): Change? =
- info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }
+ info.changes.firstOrNull { change ->
+ change.mode in LAUNCH_CHANGE_MODES && change.taskInfo?.isFreeform == true
+ }
private fun getMinimizeChange(info: TransitionInfo): Change? =
- info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }
+ info.changes.firstOrNull { change ->
+ change.mode == TRANSIT_TO_BACK && change.taskInfo?.isFreeform == true
+ }
private fun getTrampolineCloseChange(info: TransitionInfo): Change? {
if (
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index 79072a6..5a8934b 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -84,6 +84,10 @@
if (animators.isEmpty()) finishedCallback.run()
}
animators += animatorHelper.createAnimators(info, animatorFinishedCallback)
+ if (animators.isEmpty()) {
+ finishedCallback.run()
+ return
+ }
animators.forEach { it.start() }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index fc8ea87..8aab76f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -1567,6 +1567,7 @@
public boolean canUnminimizeDesktopTask(int taskId) {
BubbleTextView.RunningAppState runningAppState =
mControllers.taskbarRecentAppsController.getRunningAppState(taskId);
+ Log.d(TAG, "Task id=" + taskId + ", Running app state=" + runningAppState);
return runningAppState == RunningAppState.MINIMIZED
&& DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX.isTrue();
}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 0566f09..7574c7f 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -413,7 +413,7 @@
mMSDLPlayerWrapper = msdlPlayerWrapper;
initTransitionEndpoints(mRemoteTargetHandles[0].getTaskViewSimulator()
- .getOrientationState().getLauncherDeviceProfile());
+ .getOrientationState().getLauncherDeviceProfile(gestureState.getDisplayId()));
initStateCallbacks();
mIsTransientTaskbar = mDp.isTaskbarPresent
@@ -991,7 +991,8 @@
// both split and non-split
RecentsOrientedState orientationState = mRemoteTargetHandles[0].getTaskViewSimulator()
.getOrientationState();
- DeviceProfile dp = orientationState.getLauncherDeviceProfile();
+ DeviceProfile dp = orientationState.getLauncherDeviceProfile(
+ mGestureState.getDisplayId());
if (targets.minimizedHomeBounds != null && primaryTaskTarget != null) {
Rect overviewStackBounds = mContainerInterface
.getOverviewWindowBounds(targets.minimizedHomeBounds, primaryTaskTarget);
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
index d70d7eb..333571c 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
@@ -18,7 +18,7 @@
import android.content.Context
import android.graphics.PixelFormat
-import android.view.Display
+import android.view.Display.DEFAULT_DISPLAY
import android.view.ViewGroup
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
@@ -53,7 +53,7 @@
fun initDeviceProfile() {
deviceProfile =
- if (displayId == Display.DEFAULT_DISPLAY)
+ if (displayId == DEFAULT_DISPLAY)
InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this)
else InvariantDeviceProfile.INSTANCE[this].createDeviceProfileForSecondaryDisplay(this)
}
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
index e3c9b2b..c4e343e 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
@@ -275,7 +275,7 @@
desiredTaskId: Int,
banner: View,
): Pair<Float, Float> {
- val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
+ val snapshotParams = thumbnailViews[0].layoutParams as LinearLayout.LayoutParams
val translationX = banner.height.toFloat()
val translationY: Float
if (splitBounds == null) {
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt
index 1883649..1bbe005 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt
@@ -255,7 +255,7 @@
}
} else {
if (desiredTaskId == splitBounds.leftTopTaskId) {
- val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
+ val snapshotParams = thumbnailViews[0].layoutParams as LinearLayout.LayoutParams
val bottomRightTaskPlusDividerPercent =
(splitBounds.rightBottomTaskPercent + splitBounds.dividerPercent)
translationY =
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
index 1f9f752..67358bbb 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
@@ -26,6 +26,7 @@
import android.view.View
import android.view.View.MeasureSpec
import android.widget.FrameLayout
+import android.widget.LinearLayout
import androidx.core.util.component1
import androidx.core.util.component2
import androidx.core.view.updateLayoutParams
@@ -151,7 +152,7 @@
desiredTaskId: Int,
banner: View,
): Pair<Float, Float> {
- val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
+ val snapshotParams = thumbnailViews[0].layoutParams as LinearLayout.LayoutParams
val translationX: Float = (taskViewWidth - banner.height).toFloat()
val translationY: Float
if (splitBounds == null) {
diff --git a/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt b/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt
index 619075f..aa1c236 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt
@@ -18,17 +18,49 @@
import android.view.View.OnClickListener
import com.android.quickstep.recents.ui.viewmodel.TaskData
+import com.android.quickstep.task.thumbnail.TaskHeaderUiState
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
object TaskUiStateMapper {
/**
+ * Converts a [TaskData] object into a [TaskHeaderUiState] for display in the UI.
+ *
+ * This function handles different types of [TaskData] and determines the appropriate UI state
+ * based on the data and provided flags.
+ *
+ * @param taskData The [TaskData] to convert. Can be null or a specific subclass.
+ * @param hasHeader A flag indicating whether the UI should display a header.
+ * @param clickCloseListener A callback when the close button in the UI is clicked.
+ * @return A [TaskHeaderUiState] representing the UI state for the given task data.
+ */
+ fun toTaskHeaderState(
+ taskData: TaskData?,
+ hasHeader: Boolean,
+ clickCloseListener: OnClickListener?,
+ ): TaskHeaderUiState =
+ when {
+ taskData !is TaskData.Data -> TaskHeaderUiState.HideHeader
+ canHeaderBeCreated(taskData, hasHeader, clickCloseListener) -> {
+ TaskHeaderUiState.ShowHeader(
+ TaskHeaderUiState.ThumbnailHeader(
+ // TODO(http://b/353965691): figure out what to do when `icon` or
+ // `titleDescription` is null.
+ taskData.icon!!,
+ taskData.titleDescription!!,
+ clickCloseListener!!,
+ )
+ )
+ }
+ else -> TaskHeaderUiState.HideHeader
+ }
+
+ /**
* Converts a [TaskData] object into a [TaskThumbnailUiState] for display in the UI.
*
* This function handles different types of [TaskData] and determines the appropriate UI state
@@ -36,46 +68,24 @@
*
* @param taskData The [TaskData] to convert. Can be null or a specific subclass.
* @param isLiveTile A flag indicating whether the task data represents live tile.
- * @param hasHeader A flag indicating whether the UI should display a header.
- * @param clickCloseListener A callback when the close button in the UI is clicked.
* @return A [TaskThumbnailUiState] representing the UI state for the given task data.
*/
- fun toTaskThumbnailUiState(
- taskData: TaskData?,
- isLiveTile: Boolean,
- hasHeader: Boolean,
- clickCloseListener: OnClickListener?,
- ): TaskThumbnailUiState =
+ fun toTaskThumbnailUiState(taskData: TaskData?, isLiveTile: Boolean): TaskThumbnailUiState =
when {
taskData !is TaskData.Data -> Uninitialized
- isLiveTile -> createLiveTileState(taskData, hasHeader, clickCloseListener)
+ isLiveTile -> LiveTile
isBackgroundOnly(taskData) -> BackgroundOnly(taskData.backgroundColor)
isSnapshotSplash(taskData) ->
SnapshotSplash(
- createSnapshotState(taskData, hasHeader, clickCloseListener),
+ Snapshot(
+ taskData.thumbnailData?.thumbnail!!,
+ taskData.thumbnailData.rotation,
+ taskData.backgroundColor,
+ ),
taskData.icon,
)
- else -> Uninitialized
- }
- private fun createSnapshotState(
- taskData: TaskData.Data,
- hasHeader: Boolean,
- clickCloseListener: OnClickListener?,
- ): Snapshot =
- if (canHeaderBeCreated(taskData, hasHeader, clickCloseListener)) {
- Snapshot.WithHeader(
- taskData.thumbnailData?.thumbnail!!,
- taskData.thumbnailData.rotation,
- taskData.backgroundColor,
- ThumbnailHeader(taskData.icon!!, taskData.titleDescription!!, clickCloseListener!!),
- )
- } else {
- Snapshot.WithoutHeader(
- taskData.thumbnailData?.thumbnail!!,
- taskData.thumbnailData.rotation,
- taskData.backgroundColor,
- )
+ else -> Uninitialized
}
private fun isBackgroundOnly(taskData: TaskData.Data) =
@@ -93,17 +103,4 @@
taskData.icon != null &&
taskData.titleDescription != null &&
clickCloseListener != null
-
- private fun createLiveTileState(
- taskData: TaskData.Data,
- hasHeader: Boolean,
- clickCloseListener: OnClickListener?,
- ) =
- if (canHeaderBeCreated(taskData, hasHeader, clickCloseListener)) {
- // TODO(http://b/353965691): figure out what to do when `icon` or `titleDescription` is
- // null.
- LiveTile.WithHeader(
- ThumbnailHeader(taskData.icon!!, taskData.titleDescription!!, clickCloseListener!!)
- )
- } else LiveTile.WithoutHeader
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskContentView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskContentView.kt
new file mode 100644
index 0000000..2dbd811
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskContentView.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.content.Context
+import android.graphics.Outline
+import android.graphics.Path
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewOutlineProvider
+import android.widget.LinearLayout
+import androidx.core.view.isInvisible
+import com.android.launcher3.Flags.enableDesktopExplodedView
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.R
+import com.android.launcher3.util.ViewPool
+import com.android.quickstep.views.TaskHeaderView
+import com.android.quickstep.views.TaskThumbnailViewDeprecated
+
+/**
+ * TaskContentView is a wrapper around the TaskHeaderView and TaskThumbnailView. It is a sibling to
+ * DWB, AiAi (TaskOverlay).
+ */
+class TaskContentView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+ LinearLayout(context, attrs), ViewPool.Reusable {
+
+ private var taskHeaderView: TaskHeaderView? = null
+ private var taskThumbnailView: TaskThumbnailView? = null
+ private var taskThumbnailViewDeprecated: TaskThumbnailViewDeprecated? = null
+ private var onSizeChanged: ((width: Int, height: Int) -> Unit)? = null
+ private val outlinePath = Path()
+
+ /**
+ * Sets the outline bounds of the view. Default to use view's bound as outline when set to null.
+ */
+ var outlineBounds: Rect? = null
+ set(value) {
+ field = value
+ invalidateOutline()
+ }
+
+ private val bounds = Rect()
+
+ var cornerRadius: Float = 0f
+ set(value) {
+ field = value
+ invalidateOutline()
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ maybeCreateHeader()
+ createTaskThumbnailView()
+ }
+
+ override fun setScaleX(scaleX: Float) {
+ super.setScaleX(scaleX)
+ taskThumbnailView?.parentScaleXUpdated(scaleX)
+ }
+
+ override fun setScaleY(scaleY: Float) {
+ super.setScaleY(scaleY)
+ taskThumbnailView?.parentScaleYUpdated(scaleY)
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ clipToOutline = true
+ outlineProvider =
+ object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ val outlineRect = outlineBounds ?: bounds
+ outlinePath.apply {
+ rewind()
+ addRoundRect(
+ outlineRect.left.toFloat(),
+ outlineRect.top.toFloat(),
+ outlineRect.right.toFloat(),
+ outlineRect.bottom.toFloat(),
+ cornerRadius / scaleX,
+ cornerRadius / scaleY,
+ Path.Direction.CW,
+ )
+ }
+ outline.setPath(outlinePath)
+ }
+ }
+ }
+
+ override fun onRecycle() {
+ taskHeaderView?.isInvisible = true
+ onSizeChanged = null
+ outlineBounds = null
+ taskThumbnailView?.onRecycle()
+ taskThumbnailViewDeprecated?.onRecycle()
+ }
+
+ fun doOnSizeChange(action: (width: Int, height: Int) -> Unit) {
+ onSizeChanged = action
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ onSizeChanged?.invoke(width, height)
+ bounds.set(0, 0, w, h)
+ invalidateOutline()
+ }
+
+ private fun maybeCreateHeader() {
+ if (enableDesktopExplodedView() && taskHeaderView == null) {
+ taskHeaderView =
+ LayoutInflater.from(context).inflate(R.layout.task_header_view, this, false)
+ as TaskHeaderView
+ addView(taskHeaderView)
+ }
+ }
+
+ private fun createTaskThumbnailView() {
+ if (taskThumbnailView == null) {
+ if (enableRefactorTaskThumbnail()) {
+ taskThumbnailView =
+ LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false)
+ as TaskThumbnailView
+ addView(taskThumbnailView)
+ } else {
+ taskThumbnailViewDeprecated =
+ LayoutInflater.from(context)
+ .inflate(R.layout.task_thumbnail_deprecated, this, false)
+ as TaskThumbnailViewDeprecated
+ addView(taskThumbnailViewDeprecated)
+ }
+ }
+ }
+
+ fun setState(
+ taskHeaderState: TaskHeaderUiState,
+ taskThumbnailUiState: TaskThumbnailUiState,
+ taskId: Int?,
+ ) {
+ taskHeaderView?.setState(taskHeaderState)
+ taskThumbnailView?.setState(taskThumbnailUiState, taskId)
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskHeaderUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskHeaderUiState.kt
new file mode 100644
index 0000000..09fb540
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskHeaderUiState.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.graphics.drawable.Drawable
+import android.view.View
+
+sealed class TaskHeaderUiState {
+ data class ShowHeader(val header: ThumbnailHeader) : TaskHeaderUiState()
+
+ data object HideHeader : TaskHeaderUiState()
+
+ data class ThumbnailHeader(
+ val icon: Drawable,
+ val title: String,
+ val clickCloseListener: View.OnClickListener,
+ )
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
index db593d3..a5c9ac0 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
@@ -19,7 +19,6 @@
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.Surface
-import android.view.View.OnClickListener
import androidx.annotation.ColorInt
sealed class TaskThumbnailUiState {
@@ -27,37 +26,14 @@
data class BackgroundOnly(@ColorInt val backgroundColor: Int) : TaskThumbnailUiState()
+ data object LiveTile : TaskThumbnailUiState()
+
data class SnapshotSplash(val snapshot: Snapshot, val splash: Drawable?) :
TaskThumbnailUiState()
- sealed class LiveTile : TaskThumbnailUiState() {
- data class WithHeader(val header: ThumbnailHeader) : LiveTile()
-
- data object WithoutHeader : LiveTile()
- }
-
- sealed class Snapshot {
- abstract val bitmap: Bitmap
- abstract val thumbnailRotation: Int
- abstract val backgroundColor: Int
-
- data class WithHeader(
- override val bitmap: Bitmap,
- @Surface.Rotation override val thumbnailRotation: Int,
- @ColorInt override val backgroundColor: Int,
- val header: ThumbnailHeader,
- ) : Snapshot()
-
- data class WithoutHeader(
- override val bitmap: Bitmap,
- @Surface.Rotation override val thumbnailRotation: Int,
- @ColorInt override val backgroundColor: Int,
- ) : Snapshot()
- }
-
- data class ThumbnailHeader(
- val icon: Drawable,
- val title: String,
- val clickCloseListener: OnClickListener,
+ data class Snapshot(
+ val bitmap: Bitmap,
+ @Surface.Rotation val thumbnailRotation: Int,
+ @ColorInt val backgroundColor: Int,
)
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 0edbacc..78a16f1 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -19,32 +19,24 @@
import android.content.Context
import android.graphics.Color
import android.graphics.Matrix
-import android.graphics.Outline
-import android.graphics.Path
-import android.graphics.Rect
import android.graphics.drawable.ShapeDrawable
import android.util.AttributeSet
import android.util.Log
-import android.view.LayoutInflater
import android.view.View
-import android.view.ViewOutlineProvider
import android.widget.FrameLayout
import androidx.annotation.ColorInt
import androidx.core.view.isInvisible
-import com.android.launcher3.Flags.enableDesktopExplodedView
import com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA
import com.android.launcher3.R
import com.android.launcher3.util.MultiPropertyFactory
-import com.android.launcher3.util.ViewPool
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.views.FixedSizeImageView
-import com.android.quickstep.views.TaskThumbnailViewHeader
-class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
+class TaskThumbnailView : FrameLayout {
private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) }
private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) }
private val thumbnailView: FixedSizeImageView by lazy { findViewById(R.id.task_thumbnail) }
@@ -53,30 +45,9 @@
private val dimAlpha: MultiPropertyFactory<View> by lazy {
MultiPropertyFactory(scrimView, VIEW_ALPHA, ScrimViewAlpha.entries.size, ::maxOf)
}
- private val outlinePath = Path()
- private var onSizeChanged: ((width: Int, height: Int) -> Unit)? = null
-
- private var taskThumbnailViewHeader: TaskThumbnailViewHeader? = null
private var uiState: TaskThumbnailUiState = Uninitialized
- /**
- * Sets the outline bounds of the view. Default to use view's bound as outline when set to null.
- */
- var outlineBounds: Rect? = null
- set(value) {
- field = value
- invalidateOutline()
- }
-
- private val bounds = Rect()
-
- var cornerRadius: Float = 0f
- set(value) {
- field = value
- invalidateOutline()
- }
-
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
@@ -87,39 +58,8 @@
defStyleAttr: Int,
) : super(context, attrs, defStyleAttr)
- override fun onFinishInflate() {
- super.onFinishInflate()
- maybeCreateHeader()
- }
-
- override fun onAttachedToWindow() {
- super.onAttachedToWindow()
- clipToOutline = true
- outlineProvider =
- object : ViewOutlineProvider() {
- override fun getOutline(view: View, outline: Outline) {
- val outlineRect = outlineBounds ?: bounds
- outlinePath.apply {
- rewind()
- addRoundRect(
- outlineRect.left.toFloat(),
- outlineRect.top.toFloat(),
- outlineRect.right.toFloat(),
- outlineRect.bottom.toFloat(),
- cornerRadius / scaleX,
- cornerRadius / scaleY,
- Path.Direction.CW,
- )
- }
- outline.setPath(outlinePath)
- }
- }
- }
-
- override fun onRecycle() {
+ fun onRecycle() {
uiState = Uninitialized
- onSizeChanged = null
- outlineBounds = null
resetViews()
}
@@ -130,7 +70,7 @@
resetViews()
when (state) {
is Uninitialized -> {}
- is LiveTile -> drawLiveWindow(state)
+ is LiveTile -> drawLiveWindow()
is SnapshotSplash -> drawSnapshotSplash(state)
is BackgroundOnly -> drawBackground(state.backgroundColor)
}
@@ -155,25 +95,12 @@
splashIcon.alpha = value
}
- fun doOnSizeChange(action: (width: Int, height: Int) -> Unit) {
- onSizeChanged = action
- }
-
- override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
- super.onSizeChanged(w, h, oldw, oldh)
- onSizeChanged?.invoke(width, height)
- bounds.set(0, 0, w, h)
- invalidateOutline()
- }
-
- override fun setScaleX(scaleX: Float) {
- super.setScaleX(scaleX)
+ fun parentScaleXUpdated(scaleX: Float) {
// Splash icon should ignore scale on TTV
splashIcon.scaleX = 1 / scaleX
}
- override fun setScaleY(scaleY: Float) {
- super.setScaleY(scaleY)
+ fun parentScaleYUpdated(scaleY: Float) {
// Splash icon should ignore scale on TTV
splashIcon.scaleY = 1 / scaleY
}
@@ -187,20 +114,14 @@
splashIcon.setImageDrawable(null)
scrimView.alpha = 0f
setBackgroundColor(Color.BLACK)
- taskThumbnailViewHeader?.isInvisible = true
}
private fun drawBackground(@ColorInt background: Int) {
setBackgroundColor(background)
}
- private fun drawLiveWindow(liveTile: LiveTile) {
+ private fun drawLiveWindow() {
liveTileView.isInvisible = false
-
- if (liveTile is LiveTile.WithHeader) {
- taskThumbnailViewHeader?.isInvisible = false
- taskThumbnailViewHeader?.setHeader(liveTile.header)
- }
}
private fun drawSnapshotSplash(snapshotSplash: SnapshotSplash) {
@@ -212,11 +133,6 @@
}
private fun drawSnapshot(snapshot: Snapshot) {
- if (snapshot is Snapshot.WithHeader) {
- taskThumbnailViewHeader?.isInvisible = false
- taskThumbnailViewHeader?.setHeader(snapshot.header)
- }
-
drawBackground(snapshot.backgroundColor)
thumbnailView.setImageBitmap(snapshot.bitmap)
thumbnailView.isInvisible = false
@@ -232,16 +148,6 @@
Log.d(TAG, "[TaskThumbnailView@${Integer.toHexString(hashCode())}] $message")
}
- private fun maybeCreateHeader() {
- if (enableDesktopExplodedView() && taskThumbnailViewHeader == null) {
- taskThumbnailViewHeader =
- LayoutInflater.from(context)
- .inflate(R.layout.task_thumbnail_view_header, this, false)
- as TaskThumbnailViewHeader
- addView(taskThumbnailViewHeader)
- }
- }
-
private companion object {
const val TAG = "TaskThumbnailView"
private const val MAX_SCRIM_ALPHA = 0.4f
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index a5be89a..8954d80 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -22,6 +22,7 @@
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
+import static com.android.launcher3.Flags.enableOverviewOnConnectedDisplays;
import static com.android.launcher3.LauncherPrefs.ALLOW_ROTATION;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.SettingsCache.ROTATION_SETTING_URI;
@@ -53,6 +54,7 @@
import com.android.quickstep.BaseContainerInterface;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.fallback.window.RecentsDisplayModel;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import java.lang.annotation.Retention;
@@ -571,19 +573,25 @@
/**
* Returns the device profile based on expected launcher rotation
*/
- public DeviceProfile getLauncherDeviceProfile() {
- InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
- Point currentSize = DisplayController.INSTANCE.get(mContext).getInfo().currentSize;
-
- int width, height;
- if ((mRecentsActivityRotation == ROTATION_90 || mRecentsActivityRotation == ROTATION_270)) {
- width = Math.max(currentSize.x, currentSize.y);
- height = Math.min(currentSize.x, currentSize.y);
+ public DeviceProfile getLauncherDeviceProfile(int displayId) {
+ if (enableOverviewOnConnectedDisplays()) {
+ return RecentsDisplayModel.getINSTANCE().get(mContext).getRecentsWindowManager(
+ displayId).getDeviceProfile();
} else {
- width = Math.min(currentSize.x, currentSize.y);
- height = Math.max(currentSize.x, currentSize.y);
+ InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
+ Point currentSize = DisplayController.INSTANCE.get(mContext).getInfo().currentSize;
+
+ int width, height;
+ if ((mRecentsActivityRotation == ROTATION_90
+ || mRecentsActivityRotation == ROTATION_270)) {
+ width = Math.max(currentSize.x, currentSize.y);
+ height = Math.min(currentSize.x, currentSize.y);
+ } else {
+ width = Math.min(currentSize.x, currentSize.y);
+ height = Math.max(currentSize.x, currentSize.y);
+ }
+ return idp.getBestMatch(width, height, mRecentsActivityRotation);
}
- return idp.getBestMatch(width, height, mRecentsActivityRotation);
}
private static String nameAndAddress(Object obj) {
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 96a5733..d6e553d 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -192,7 +192,7 @@
taskViewHeight: Int,
isPrimaryTaskSplitting: Boolean,
) {
- val snapshot = taskContainer.snapshotView
+ val taskContentView = taskContainer.taskContentView
val iconView: View = taskContainer.iconView.asView()
if (enableRefactorTaskThumbnail()) {
builder.add(
@@ -241,7 +241,11 @@
val centerThumbnailTranslationX: Float = (taskViewWidth - snapshotViewSize.x) / 2f
val finalScaleX: Float = taskViewWidth.toFloat() / snapshotViewSize.x
builder.add(
- ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_X, centerThumbnailTranslationX)
+ ObjectAnimator.ofFloat(
+ taskContentView,
+ View.TRANSLATION_X,
+ centerThumbnailTranslationX,
+ )
)
if (!enableOverviewIconMenu()) {
// icons are anchored from Gravity.END, so need to use negative translation
@@ -250,15 +254,17 @@
ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, -centerIconTranslationX)
)
}
- builder.add(ObjectAnimator.ofFloat(snapshot, View.SCALE_X, finalScaleX))
+ builder.add(ObjectAnimator.ofFloat(taskContentView, View.SCALE_X, finalScaleX))
// Reset other dimensions
// TODO(b/271468547), can't set Y translate to 0, need to account for top space
- snapshot.scaleY = 1f
+ taskContentView.scaleY = 1f
val translateYResetVal: Float =
if (!isPrimaryTaskSplitting) 0f
else deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat()
- builder.add(ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_Y, translateYResetVal))
+ builder.add(
+ ObjectAnimator.ofFloat(taskContentView, View.TRANSLATION_Y, translateYResetVal)
+ )
} else {
val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
// Center view first so scaling happens uniformly, alternatively we can move pivotY to 0
@@ -281,18 +287,22 @@
}
val finalScaleY: Float = thumbnailSize.toFloat() / snapshotViewSize.y
builder.add(
- ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_Y, centerThumbnailTranslationY)
+ ObjectAnimator.ofFloat(
+ taskContentView,
+ View.TRANSLATION_Y,
+ centerThumbnailTranslationY,
+ )
)
if (!enableOverviewIconMenu()) {
// icons are anchored from Gravity.END, so need to use negative translation
builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, 0f))
}
- builder.add(ObjectAnimator.ofFloat(snapshot, View.SCALE_Y, finalScaleY))
+ builder.add(ObjectAnimator.ofFloat(taskContentView, View.SCALE_Y, finalScaleY))
// Reset other dimensions
- snapshot.scaleX = 1f
- builder.add(ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_X, 0f))
+ taskContentView.scaleX = 1f
+ builder.add(ObjectAnimator.ofFloat(taskContentView, View.TRANSLATION_X, 0f))
}
}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index e0ea518..1d035e9 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -54,6 +54,7 @@
import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
import com.android.quickstep.recents.ui.viewmodel.DesktopTaskViewModel
import com.android.quickstep.recents.ui.viewmodel.TaskData
+import com.android.quickstep.task.thumbnail.TaskContentView
import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.RecentsOrientedState
@@ -69,27 +70,14 @@
) {
private val contentViewFullscreenParams = FullscreenDrawParams(context)
- private val taskThumbnailViewDeprecatedPool =
- if (!enableRefactorTaskThumbnail()) {
- ViewPool<TaskThumbnailViewDeprecated>(
- context,
- this,
- R.layout.task_thumbnail_deprecated,
- VIEW_POOL_MAX_SIZE,
- VIEW_POOL_INITIAL_SIZE,
- )
- } else null
-
- private val taskThumbnailViewPool =
- if (enableRefactorTaskThumbnail()) {
- ViewPool<TaskThumbnailView>(
- context,
- this,
- R.layout.task_thumbnail,
- VIEW_POOL_MAX_SIZE,
- VIEW_POOL_INITIAL_SIZE,
- )
- } else null
+ private val taskContentViewPool =
+ ViewPool<TaskContentView>(
+ context,
+ this,
+ R.layout.task_content_view,
+ VIEW_POOL_MAX_SIZE,
+ VIEW_POOL_INITIAL_SIZE,
+ )
private val tempPointF = PointF()
private val lastComputedTaskSize = Rect()
@@ -239,7 +227,7 @@
// for all cases where the progress is non-zero.
if (explodeProgress == 0.0f || explodeProgress == 1.0f) {
// Reset scaling and translation that may have been applied during animation.
- it.snapshotView.apply {
+ it.taskContentView.apply {
scaleX = 1.0f
scaleY = 1.0f
translationX = 0.0f
@@ -247,7 +235,7 @@
}
// Position the task to the same position as it would be on the desktop
- it.snapshotView.updateLayoutParams<LayoutParams> {
+ it.taskContentView?.updateLayoutParams<LayoutParams> {
gravity = Gravity.LEFT or Gravity.TOP
width = taskWidth.toInt()
height = taskHeight.toInt()
@@ -258,7 +246,7 @@
if (
enableDesktopRecentsTransitionsCornersBugfix() && enableRefactorTaskThumbnail()
) {
- it.thumbnailView.outlineBounds =
+ it.taskContentView?.outlineBounds =
if (intersects(overviewTaskPosition, screenRect))
Rect(overviewTaskPosition).apply {
intersectUnchecked(screenRect)
@@ -275,7 +263,7 @@
} else {
// During the animation, apply translation and scale such that the view is
// transformed to where we want, without triggering layout.
- it.snapshotView.apply {
+ it.taskContentView.apply {
pivotX = 0.0f
pivotY = 0.0f
translationX = taskLeft - left
@@ -308,17 +296,19 @@
val backgroundViewIndex = contentView.indexOfChild(backgroundView)
taskContainers =
tasks.map { task ->
+ val taskContentView = taskContentViewPool.view
+ contentView.addView(taskContentView, backgroundViewIndex + 1)
val snapshotView =
if (enableRefactorTaskThumbnail()) {
- taskThumbnailViewPool!!.view
+ taskContentView.findViewById<TaskThumbnailView>(R.id.snapshot)
} else {
- taskThumbnailViewDeprecatedPool!!.view
+ taskContentView.findViewById<TaskThumbnailViewDeprecated>(R.id.snapshot)
}
- contentView.addView(snapshotView, backgroundViewIndex + 1)
TaskContainer(
this,
task,
+ taskContentView,
snapshotView,
iconView,
TransformingTouchDelegate(iconView.asView()),
@@ -465,12 +455,8 @@
}
private fun removeAndRecycleThumbnailView(taskContainer: TaskContainer) {
- contentView.removeView(taskContainer.snapshotView)
- if (enableRefactorTaskThumbnail()) {
- taskThumbnailViewPool!!.recycle(taskContainer.thumbnailView)
- } else {
- taskThumbnailViewDeprecatedPool!!.recycle(taskContainer.thumbnailViewDeprecated)
- }
+ contentView.removeView(taskContainer.taskContentView)
+ taskContentViewPool.recycle(taskContainer.taskContentView)
}
private fun updateTaskPositions() {
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index a8eee0a..71a4dde 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -24,7 +24,6 @@
import android.view.ViewStub
import com.android.internal.jank.Cuj
import com.android.launcher3.Flags.enableOverviewIconMenu
-import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.util.RunnableList
@@ -78,8 +77,8 @@
val splitBoundsConfig = splitBoundsConfig ?: return
val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID
pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds(
- leftTopTaskContainer.snapshotView,
- rightBottomTaskContainer.snapshotView,
+ leftTopTaskContainer.taskContentView,
+ rightBottomTaskContainer.taskContentView,
widthSize,
heightSize,
splitBoundsConfig,
@@ -95,12 +94,8 @@
override fun inflateViewStubs() {
super.inflateViewStubs()
- findViewById<ViewStub>(R.id.bottomright_snapshot)
- ?.apply {
- layoutResource =
- if (enableRefactorTaskThumbnail()) R.layout.task_thumbnail
- else R.layout.task_thumbnail_deprecated
- }
+ findViewById<ViewStub>(R.id.bottomright_task_content_view)
+ ?.apply { layoutResource = R.layout.task_content_view }
?.inflate()
findViewById<ViewStub>(R.id.bottomRight_icon)
?.apply {
@@ -128,6 +123,7 @@
listOf(
createTaskContainer(
primaryTask,
+ R.id.task_content_view,
R.id.snapshot,
R.id.icon,
R.id.show_windows,
@@ -137,7 +133,8 @@
),
createTaskContainer(
secondaryTask,
- R.id.bottomright_snapshot,
+ R.id.bottomright_task_content_view,
+ R.id.snapshot,
R.id.bottomRight_icon,
R.id.show_windows_right,
R.id.bottomRight_digital_wellbeing_toast,
@@ -240,8 +237,8 @@
leftTopTaskContainer.iconView.asView(),
rightBottomTaskContainer.iconView.asView(),
taskIconHeight,
- leftTopTaskContainer.snapshotView.measuredWidth,
- leftTopTaskContainer.snapshotView.measuredHeight,
+ leftTopTaskContainer.taskContentView.measuredWidth,
+ leftTopTaskContainer.taskContentView.measuredHeight,
measuredHeight,
measuredWidth,
isRtl,
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 2c84e52..3b94380 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -4948,6 +4948,10 @@
}
setPivotX(mTempPointF.x);
setPivotY(mTempPointF.y);
+ if (enableGridOnlyOverview()) {
+ runActionOnRemoteHandles(remoteTargetHandle ->
+ remoteTargetHandle.getTaskViewSimulator().setPivotOverride(mTempPointF));
+ }
}
/**
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 2b9d036..6d7ae70 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -28,6 +28,7 @@
import com.android.quickstep.ViewUtils.addAccessibleChildToList
import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper
import com.android.quickstep.recents.ui.viewmodel.TaskData
+import com.android.quickstep.task.thumbnail.TaskContentView
import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -36,6 +37,7 @@
class TaskContainer(
val taskView: TaskView,
val task: Task,
+ val taskContentView: TaskContentView,
val snapshotView: View,
val iconView: TaskViewIcon,
/**
@@ -104,8 +106,8 @@
fun destroy() {
digitalWellBeingToast?.destroy()
- snapshotView.scaleX = 1f
- snapshotView.scaleY = 1f
+ taskContentView.scaleX = 1f
+ taskContentView.scaleY = 1f
overlay.destroy()
if (enableRefactorTaskThumbnail()) {
isThumbnailValid = false
@@ -136,13 +138,9 @@
hasHeader: Boolean,
clickCloseListener: OnClickListener?,
) {
- thumbnailView.setState(
- TaskUiStateMapper.toTaskThumbnailUiState(
- state,
- liveTile,
- hasHeader,
- clickCloseListener,
- ),
+ taskContentView.setState(
+ TaskUiStateMapper.toTaskHeaderState(state, hasHeader, clickCloseListener),
+ TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile),
state?.taskId,
)
thumbnailData = if (state is TaskData.Data) state.thumbnailData else null
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt b/quickstep/src/com/android/quickstep/views/TaskHeaderView.kt
similarity index 66%
rename from quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt
rename to quickstep/src/com/android/quickstep/views/TaskHeaderView.kt
index 9a8805b..0427402 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskHeaderView.kt
@@ -22,19 +22,29 @@
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
+import androidx.core.view.isGone
import com.android.launcher3.R
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
+import com.android.quickstep.task.thumbnail.TaskHeaderUiState
-class TaskThumbnailViewHeader
-@JvmOverloads
-constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
+class TaskHeaderView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+ FrameLayout(context, attrs) {
private val headerTitleView: TextView by lazy { findViewById(R.id.header_app_title) }
private val headerIconView: ImageView by lazy { findViewById(R.id.header_app_icon) }
private val headerCloseButton: ImageButton by lazy { findViewById(R.id.header_close_button) }
- fun setHeader(header: ThumbnailHeader) {
- headerTitleView.setText(header.title)
+ fun setState(taskHeaderState: TaskHeaderUiState) {
+ when (taskHeaderState) {
+ is TaskHeaderUiState.ShowHeader -> {
+ setHeader(taskHeaderState.header)
+ isGone = false
+ }
+ TaskHeaderUiState.HideHeader -> isGone = true
+ }
+ }
+
+ private fun setHeader(header: TaskHeaderUiState.ThumbnailHeader) {
+ headerTitleView.text = header.title
headerIconView.setImageDrawable(header.icon)
headerCloseButton.setOnClickListener(header.clickCloseListener)
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 86e1305..91a4273 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -85,6 +85,7 @@
import com.android.quickstep.recents.ui.viewmodel.TaskData
import com.android.quickstep.recents.ui.viewmodel.TaskTileUiState
import com.android.quickstep.recents.ui.viewmodel.TaskViewModel
+import com.android.quickstep.task.thumbnail.TaskContentView
import com.android.quickstep.util.ActiveGestureErrorDetector
import com.android.quickstep.util.ActiveGestureLog
import com.android.quickstep.util.BorderAnimator
@@ -721,13 +722,10 @@
}
protected open fun inflateViewStubs() {
- findViewById<ViewStub>(R.id.snapshot)
- ?.apply {
- layoutResource =
- if (enableRefactorTaskThumbnail()) R.layout.task_thumbnail
- else R.layout.task_thumbnail_deprecated
- }
+ findViewById<ViewStub>(R.id.task_content_view)
+ ?.apply { layoutResource = R.layout.task_content_view }
?.inflate()
+
findViewById<ViewStub>(R.id.icon)
?.apply {
layoutResource =
@@ -845,6 +843,7 @@
listOf(
createTaskContainer(
task,
+ R.id.task_content_view,
R.id.snapshot,
R.id.icon,
R.id.show_windows,
@@ -876,8 +875,9 @@
taskContainers.forEach { container ->
container.bind()
if (enableRefactorTaskThumbnail()) {
- container.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
- container.thumbnailView.doOnSizeChange { width, height ->
+ container.taskContentView.cornerRadius =
+ thumbnailFullscreenParams.currentCornerRadius
+ container.taskContentView.doOnSizeChange { width, height ->
updateThumbnailValidity(container)
updateThumbnailMatrix(container, width, height)
}
@@ -903,6 +903,7 @@
protected fun createTaskContainer(
task: Task,
+ @IdRes taskContentViewId: Int,
@IdRes thumbnailViewId: Int,
@IdRes iconViewId: Int,
@IdRes showWindowViewId: Int,
@@ -911,10 +912,12 @@
taskOverlayFactory: TaskOverlayFactory,
): TaskContainer {
val iconView = findViewById<View>(iconViewId) as TaskViewIcon
+ val taskContentView = findViewById<TaskContentView>(taskContentViewId)
return TaskContainer(
this,
task,
- findViewById(thumbnailViewId),
+ taskContentView,
+ taskContentView.findViewById(thumbnailViewId),
iconView,
TransformingTouchDelegate(iconView.asView()),
stagePosition,
@@ -1002,7 +1005,7 @@
protected open fun updateThumbnailSize() {
// TODO(b/271468547), we should default to setting translations only on the snapshot instead
// of a hybrid of both margins and translations
- firstTaskContainer?.snapshotView?.updateLayoutParams<LayoutParams> {
+ firstTaskContainer?.taskContentView?.updateLayoutParams<LayoutParams> {
topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
}
taskContainers.forEach { it.digitalWellBeingToast?.setupLayout() }
@@ -1016,11 +1019,11 @@
val thumbnailBounds = Rect()
if (relativeToDragLayer) {
container.dragLayer.getDescendantRectRelativeToSelf(
- it.snapshotView,
+ it.taskContentView,
thumbnailBounds,
)
} else {
- thumbnailBounds.set(it.snapshotView)
+ thumbnailBounds.set(it.taskContentView)
}
bounds.union(thumbnailBounds)
}
@@ -1709,7 +1712,7 @@
updateFullscreenParams(thumbnailFullscreenParams)
taskContainers.forEach {
if (enableRefactorTaskThumbnail()) {
- it.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
+ it.taskContentView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
} else {
it.thumbnailViewDeprecated.setFullscreenParams(thumbnailFullscreenParams)
}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/SplashHelper.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/SplashHelper.kt
new file mode 100644
index 0000000..8cc09d4
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/SplashHelper.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+
+object SplashHelper {
+ private val BITMAP_RECT_COLORS = listOf(Color.GREEN, Color.RED, Color.BLUE, Color.CYAN)
+
+ fun createSplash(): Bitmap = createBitmap(width = 20, height = 20, rectColorRotation = 1)
+
+ fun createBitmap(width: Int, height: Int, rectColorRotation: Int = 0): Bitmap =
+ Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
+ Canvas(this).apply {
+ val paint = Paint()
+ paint.color = BITMAP_RECT_COLORS[rectColorRotation % 4]
+ drawRect(0f, 0f, width / 2f, height / 2f, paint)
+ paint.color = BITMAP_RECT_COLORS[(1 + rectColorRotation) % 4]
+ drawRect(width / 2f, 0f, width.toFloat(), height / 2f, paint)
+ paint.color = BITMAP_RECT_COLORS[(2 + rectColorRotation) % 4]
+ drawRect(0f, height / 2f, width / 2f, height.toFloat(), paint)
+ paint.color = BITMAP_RECT_COLORS[(3 + rectColorRotation) % 4]
+ drawRect(width / 2f, height / 2f, width.toFloat(), height.toFloat(), paint)
+ }
+ }
+}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskContentViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskContentViewScreenshotTest.kt
new file mode 100644
index 0000000..d36faa2
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskContentViewScreenshotTest.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.content.Context
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.view.LayoutInflater
+import com.android.launcher3.R
+import com.android.quickstep.task.thumbnail.SplashHelper.createSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays
+import platform.test.screenshot.ViewScreenshotTestRule
+import platform.test.screenshot.getEmulatedDevicePathConfig
+
+/** Screenshot tests for [TaskContentView]. */
+@RunWith(ParameterizedAndroidJunit4::class)
+class TaskContentViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
+
+ @get:Rule
+ val screenshotRule =
+ ViewScreenshotTestRule(
+ emulationSpec,
+ ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
+ )
+
+ @Test
+ fun taskContentView_recyclesToUninitialized() {
+ screenshotRule.screenshotTest("taskContentView_uninitialized") { activity ->
+ activity.actionBar?.hide()
+ val taskContentView = createTaskContentView(activity)
+ taskContentView.setState(
+ TaskHeaderUiState.HideHeader,
+ BackgroundOnly(Color.YELLOW),
+ null,
+ )
+ taskContentView.onRecycle()
+ taskContentView
+ }
+ }
+
+ @Test
+ fun taskContentView_shows_thumbnail_and_header() {
+ screenshotRule.screenshotTest("taskContentView_shows_thumbnail_and_header") { activity ->
+ activity.actionBar?.hide()
+ createTaskContentView(activity).apply {
+ setState(
+ TaskHeaderUiState.ShowHeader(
+ TaskHeaderUiState.ThumbnailHeader(
+ BitmapDrawable(activity.resources, createSplash()),
+ "test",
+ ) {}
+ ),
+ BackgroundOnly(Color.YELLOW),
+ null,
+ )
+ }
+ }
+ }
+
+ @Test
+ fun taskContentView_scaled_roundRoundedCorners() {
+ screenshotRule.screenshotTest("taskContentView_scaledRoundedCorners") { activity ->
+ activity.actionBar?.hide()
+ createTaskContentView(activity).apply {
+ scaleX = 0.75f
+ scaleY = 0.3f
+ setState(TaskHeaderUiState.HideHeader, BackgroundOnly(Color.YELLOW), null)
+ }
+ }
+ }
+
+ private fun createTaskContentView(context: Context): TaskContentView {
+ val taskContentView =
+ LayoutInflater.from(context).inflate(R.layout.task_content_view, null, false)
+ as TaskContentView
+ taskContentView.cornerRadius = CORNER_RADIUS
+ return taskContentView
+ }
+
+ companion object {
+ @Parameters(name = "{0}")
+ @JvmStatic
+ fun getTestSpecs() =
+ DeviceEmulationSpec.forDisplays(
+ Displays.Phone,
+ isDarkTheme = false,
+ isLandscape = false,
+ )
+
+ const val CORNER_RADIUS = 56f
+ }
+}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskHeaderViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskHeaderViewScreenshotTest.kt
new file mode 100644
index 0000000..e30554e
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskHeaderViewScreenshotTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.content.Context
+import android.graphics.drawable.BitmapDrawable
+import android.view.LayoutInflater
+import com.android.launcher3.R
+import com.android.quickstep.task.thumbnail.SplashHelper.createSplash
+import com.android.quickstep.views.TaskHeaderView
+import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays
+import platform.test.screenshot.ViewScreenshotTestRule
+import platform.test.screenshot.getEmulatedDevicePathConfig
+
+/** Screenshot tests for [TaskHeaderView]. */
+@RunWith(ParameterizedAndroidJunit4::class)
+class TaskHeaderViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
+ @get:Rule
+ val screenshotRule =
+ ViewScreenshotTestRule(
+ emulationSpec,
+ ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
+ )
+
+ @Test
+ fun taskHeaderView_showHeader() {
+ screenshotRule.screenshotTest("taskHeaderView_showHeader") { activity ->
+ activity.actionBar?.hide()
+ createTaskHeaderView(activity).apply {
+ setState(
+ TaskHeaderUiState.ShowHeader(
+ TaskHeaderUiState.ThumbnailHeader(
+ BitmapDrawable(activity.resources, createSplash()),
+ "Example",
+ ) {}
+ )
+ )
+ }
+ }
+ }
+
+ private fun createTaskHeaderView(context: Context): TaskHeaderView {
+ val taskHeaderView =
+ LayoutInflater.from(context).inflate(R.layout.task_header_view, null, false)
+ as TaskHeaderView
+ return taskHeaderView
+ }
+
+ companion object {
+ @Parameters(name = "{0}")
+ @JvmStatic
+ fun getTestSpecs() =
+ DeviceEmulationSpec.forDisplays(
+ Displays.Tablet,
+ isDarkTheme = false,
+ isLandscape = true,
+ )
+ }
+}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
index 80b2c16..45df735 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
@@ -16,16 +16,14 @@
package com.android.quickstep.task.thumbnail
import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
-import android.graphics.Paint
import android.graphics.drawable.BitmapDrawable
import android.view.LayoutInflater
import android.view.Surface.ROTATION_0
-import androidx.core.graphics.set
import com.android.launcher3.R
+import com.android.quickstep.task.thumbnail.SplashHelper.createBitmap
+import com.android.quickstep.task.thumbnail.SplashHelper.createSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
@@ -90,23 +88,25 @@
}
@Test
- fun taskThumbnailView_liveTile_withoutHeader() {
+ fun taskThumbnailView_liveTile() {
screenshotRule.screenshotTest("taskThumbnailView_liveTile") { activity ->
activity.actionBar?.hide()
- createTaskThumbnailView(activity).apply {
- setState(TaskThumbnailUiState.LiveTile.WithoutHeader)
- }
+ createTaskThumbnailView(activity).apply { setState(TaskThumbnailUiState.LiveTile) }
}
}
@Test
- fun taskThumbnailView_image_withoutHeader() {
+ fun taskThumbnailView_image() {
screenshotRule.screenshotTest("taskThumbnailView_image") { activity ->
activity.actionBar?.hide()
createTaskThumbnailView(activity).apply {
setState(
SnapshotSplash(
- Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY),
+ Snapshot(
+ createBitmap(VIEW_ENV_WIDTH, VIEW_ENV_HEIGHT),
+ ROTATION_0,
+ Color.DKGRAY,
+ ),
null,
)
)
@@ -115,14 +115,14 @@
}
@Test
- fun taskThumbnailView_image_withoutHeader_withImageMatrix() {
+ fun taskThumbnailView_image_withImageMatrix() {
screenshotRule.screenshotTest("taskThumbnailView_image_withMatrix") { activity ->
activity.actionBar?.hide()
createTaskThumbnailView(activity).apply {
val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200
setState(
SnapshotSplash(
- Snapshot.WithoutHeader(
+ Snapshot(
createBitmap(
width = VIEW_ENV_WIDTH / 2,
height = lessThanHeightMatchingAspectRatio,
@@ -139,13 +139,17 @@
}
@Test
- fun taskThumbnailView_splash_withoutHeader() {
+ fun taskThumbnailView_splash() {
screenshotRule.screenshotTest("taskThumbnailView_partial_splash") { activity ->
activity.actionBar?.hide()
createTaskThumbnailView(activity).apply {
setState(
SnapshotSplash(
- Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY),
+ Snapshot(
+ createBitmap(VIEW_ENV_WIDTH, VIEW_ENV_HEIGHT),
+ ROTATION_0,
+ Color.DKGRAY,
+ ),
BitmapDrawable(activity.resources, createSplash()),
)
)
@@ -155,14 +159,14 @@
}
@Test
- fun taskThumbnailView_splash_withoutHeader_withImageMatrix() {
+ fun taskThumbnailView_splash_withImageMatrix() {
screenshotRule.screenshotTest("taskThumbnailView_partial_splash_withMatrix") { activity ->
activity.actionBar?.hide()
createTaskThumbnailView(activity).apply {
val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200
setState(
SnapshotSplash(
- Snapshot.WithoutHeader(
+ Snapshot(
createBitmap(
width = VIEW_ENV_WIDTH / 2,
height = lessThanHeightMatchingAspectRatio,
@@ -229,31 +233,9 @@
val taskThumbnailView =
LayoutInflater.from(context).inflate(R.layout.task_thumbnail, null, false)
as TaskThumbnailView
- taskThumbnailView.cornerRadius = CORNER_RADIUS
return taskThumbnailView
}
- private fun createSplash() = createBitmap(width = 20, height = 20, rectColorRotation = 1)
-
- private fun createBitmap(
- width: Int = VIEW_ENV_WIDTH,
- height: Int = VIEW_ENV_HEIGHT,
- rectColorRotation: Int = 0,
- ) =
- Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
- Canvas(this).apply {
- val paint = Paint()
- paint.color = BITMAP_RECT_COLORS[rectColorRotation % 4]
- drawRect(0f, 0f, width / 2f, height / 2f, paint)
- paint.color = BITMAP_RECT_COLORS[(1 + rectColorRotation) % 4]
- drawRect(width / 2f, 0f, width.toFloat(), height / 2f, paint)
- paint.color = BITMAP_RECT_COLORS[(2 + rectColorRotation) % 4]
- drawRect(0f, height / 2f, width / 2f, height.toFloat(), paint)
- paint.color = BITMAP_RECT_COLORS[(3 + rectColorRotation) % 4]
- drawRect(width / 2f, height / 2f, width.toFloat(), height.toFloat(), paint)
- }
- }
-
companion object {
@Parameters(name = "{0}")
@JvmStatic
@@ -264,8 +246,6 @@
isLandscape = false,
)
- const val CORNER_RADIUS = 56f
- val BITMAP_RECT_COLORS = listOf(Color.GREEN, Color.RED, Color.BLUE, Color.CYAN)
const val VIEW_ENV_WIDTH = 1440
const val VIEW_ENV_HEIGHT = 3120
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
index 42adfec..d2abed8 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
@@ -35,6 +35,7 @@
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.TaskOverlayFactory.TaskOverlay
import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.task.thumbnail.TaskContentView
import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.views.RecentsView
import com.android.quickstep.views.TaskContainer
@@ -198,6 +199,7 @@
return TaskContainer(
taskView,
task,
+ mock<TaskContentView>(),
if (enableRefactorTaskThumbnail()) mock<TaskThumbnailView>()
else mock<TaskThumbnailViewDeprecated>(),
mock<TaskViewIcon>(),
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt
index 7ca194a..b49923f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt
@@ -25,10 +25,10 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.launcher3.Flags
import com.android.quickstep.recents.ui.viewmodel.TaskData
+import com.android.quickstep.task.thumbnail.TaskHeaderUiState
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -37,37 +37,21 @@
@RunWith(AndroidJUnit4::class)
class TaskUiStateMapperTest {
+ /** TaskHeaderUiState */
@Test
- fun taskData_isNull_returns_Uninitialized() {
+ fun taskData_isNull_returns_HideHeader() {
val result =
- TaskUiStateMapper.toTaskThumbnailUiState(
+ TaskUiStateMapper.toTaskHeaderState(
taskData = null,
- isLiveTile = false,
hasHeader = false,
clickCloseListener = null,
)
- assertThat(result).isEqualTo(TaskThumbnailUiState.Uninitialized)
- }
-
- @Test
- fun taskData_isLiveTile_returns_LiveTile() {
- val inputs =
- listOf(TASK_DATA, TASK_DATA.copy(thumbnailData = null), TASK_DATA.copy(isLocked = true))
- inputs.forEach { input ->
- val result =
- TaskUiStateMapper.toTaskThumbnailUiState(
- taskData = input,
- isLiveTile = true,
- hasHeader = false,
- clickCloseListener = null,
- )
- assertThat(result).isEqualTo(LiveTile.WithoutHeader)
- }
+ assertThat(result).isEqualTo(TaskHeaderUiState.HideHeader)
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
@Test
- fun taskData_isLiveTileWithHeader_returns_LiveTileWithHeader() {
+ fun taskData_hasHeader_and_taskData_returnsShowHeader() {
val inputs =
listOf(
TASK_DATA,
@@ -77,14 +61,18 @@
)
val closeCallback = View.OnClickListener {}
val expected =
- LiveTile.WithHeader(
- header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION, closeCallback)
+ TaskHeaderUiState.ShowHeader(
+ header =
+ TaskHeaderUiState.ThumbnailHeader(
+ TASK_ICON,
+ TASK_TITLE_DESCRIPTION,
+ closeCallback,
+ )
)
inputs.forEach { taskData ->
val result =
- TaskUiStateMapper.toTaskThumbnailUiState(
+ TaskUiStateMapper.toTaskHeaderState(
taskData = taskData,
- isLiveTile = true,
hasHeader = true,
clickCloseListener = closeCallback,
)
@@ -94,7 +82,7 @@
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
@Test
- fun taskData_isLiveTileWithHeader_missingHeaderData_returns_LiveTileWithoutHeader() {
+ fun taskData_hasHeader_emptyTaskData_returns_HideHeader() {
val inputs =
listOf(
TASK_DATA.copy(icon = null),
@@ -104,30 +92,42 @@
inputs.forEach { taskData ->
val result =
- TaskUiStateMapper.toTaskThumbnailUiState(
+ TaskUiStateMapper.toTaskHeaderState(
taskData = taskData,
- isLiveTile = true,
hasHeader = true,
clickCloseListener = {},
)
- assertThat(result).isEqualTo(LiveTile.WithoutHeader)
+ assertThat(result).isEqualTo(TaskHeaderUiState.HideHeader)
+ }
+ }
+
+ /** TaskThumbnailUiState */
+ @Test
+ fun taskData_isNull_returns_Uninitialized() {
+ val result = TaskUiStateMapper.toTaskThumbnailUiState(taskData = null, isLiveTile = false)
+ assertThat(result).isEqualTo(TaskThumbnailUiState.Uninitialized)
+ }
+
+ @Test
+ fun taskData_isLiveTile_returns_LiveTile() {
+ val inputs =
+ listOf(TASK_DATA, TASK_DATA.copy(thumbnailData = null), TASK_DATA.copy(isLocked = true))
+ inputs.forEach { input ->
+ val result =
+ TaskUiStateMapper.toTaskThumbnailUiState(taskData = input, isLiveTile = true)
+ assertThat(result).isEqualTo(LiveTile)
}
}
@Test
fun taskData_isStaticTile_returns_SnapshotSplash() {
val result =
- TaskUiStateMapper.toTaskThumbnailUiState(
- taskData = TASK_DATA,
- isLiveTile = false,
- hasHeader = false,
- clickCloseListener = null,
- )
+ TaskUiStateMapper.toTaskThumbnailUiState(taskData = TASK_DATA, isLiveTile = false)
val expected =
TaskThumbnailUiState.SnapshotSplash(
snapshot =
- Snapshot.WithoutHeader(
+ Snapshot(
backgroundColor = TASK_BACKGROUND_COLOR,
bitmap = TASK_THUMBNAIL,
thumbnailRotation = Surface.ROTATION_0,
@@ -138,72 +138,12 @@
assertThat(result).isEqualTo(expected)
}
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
- @Test
- fun taskData_isStaticTile_withHeader_returns_SnapshotSplashWithHeader() {
- val inputs = listOf(TASK_DATA, TASK_DATA.copy(title = null))
- val closeCallback = View.OnClickListener {}
- val expected =
- TaskThumbnailUiState.SnapshotSplash(
- snapshot =
- Snapshot.WithHeader(
- backgroundColor = TASK_BACKGROUND_COLOR,
- bitmap = TASK_THUMBNAIL,
- thumbnailRotation = Surface.ROTATION_0,
- header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION, closeCallback),
- ),
- splash = TASK_ICON,
- )
- inputs.forEach { taskData ->
- val result =
- TaskUiStateMapper.toTaskThumbnailUiState(
- taskData = taskData,
- isLiveTile = false,
- hasHeader = true,
- clickCloseListener = closeCallback,
- )
- assertThat(result).isEqualTo(expected)
- }
- }
-
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
- @Test
- fun taskData_isStaticTile_missingHeaderData_returns_SnapshotSplashWithoutHeader() {
- val inputs =
- listOf(
- TASK_DATA.copy(titleDescription = null, icon = null),
- TASK_DATA.copy(titleDescription = null),
- TASK_DATA.copy(icon = null),
- )
- val expected =
- Snapshot.WithoutHeader(
- backgroundColor = TASK_BACKGROUND_COLOR,
- thumbnailRotation = Surface.ROTATION_0,
- bitmap = TASK_THUMBNAIL,
- )
- inputs.forEach { taskData ->
- val result =
- TaskUiStateMapper.toTaskThumbnailUiState(
- taskData = taskData,
- isLiveTile = false,
- hasHeader = true,
- clickCloseListener = {},
- )
-
- assertThat(result).isInstanceOf(TaskThumbnailUiState.SnapshotSplash::class.java)
- result as TaskThumbnailUiState.SnapshotSplash
- assertThat(result.snapshot).isEqualTo(expected)
- }
- }
-
@Test
fun taskData_thumbnailIsNull_returns_BackgroundOnly() {
val result =
TaskUiStateMapper.toTaskThumbnailUiState(
taskData = TASK_DATA.copy(thumbnailData = null),
isLiveTile = false,
- hasHeader = false,
- clickCloseListener = null,
)
val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR)
@@ -216,8 +156,6 @@
TaskUiStateMapper.toTaskThumbnailUiState(
taskData = TASK_DATA.copy(isLocked = true),
isLiveTile = false,
- hasHeader = false,
- clickCloseListener = null,
)
val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR)
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index a523e02..7b73be7 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -36,6 +36,7 @@
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.quickstep.TaskOverlayFactory.TaskOverlay
+import com.android.quickstep.task.thumbnail.TaskContentView
import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.views.LauncherRecentsView
import com.android.quickstep.views.RecentsViewContainer
@@ -248,6 +249,7 @@
TaskContainer(
taskView,
task,
+ mock<TaskContentView>(),
if (enableRefactorTaskThumbnail()) mock<TaskThumbnailView>()
else mock<TaskThumbnailViewDeprecated>(),
mock<TaskViewIcon>(),
diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
index 2db94f6..5aaed7d 100644
--- a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
@@ -37,6 +37,7 @@
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.quickstep.TaskOverlayFactory.TaskOverlay
+import com.android.quickstep.task.thumbnail.TaskContentView
import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.views.LauncherRecentsView
import com.android.quickstep.views.RecentsViewContainer
@@ -246,6 +247,7 @@
TaskContainer(
taskView,
task,
+ mock<TaskContentView>(),
if (enableRefactorTaskThumbnail()) mock<TaskThumbnailView>()
else mock<TaskThumbnailViewDeprecated>(),
mock<TaskViewIcon>(),
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 1af48a9..b207d4a 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -100,11 +100,9 @@
@After
public void tearDown() {
- runOnRecentsView(recentsView -> {
- if (recentsView != null) {
- recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(false);
- }
- });
+ runOnRecentsView(recentsView ->
+ recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(false),
+ /* forTearDown= */ true);
}
public static void startTestApps() throws Exception {
@@ -661,18 +659,31 @@
}
private <T> T getFromRecentsView(Function<RecentsView, T> f) {
+ return getFromRecentsView(f, false);
+ }
+
+ private <T> T getFromRecentsView(Function<RecentsView, T> f, boolean forTearDown) {
if (enableLauncherOverviewInWindow()) {
- return getFromRecentsWindow(
- recentsWindowManager -> f.apply(recentsWindowManager.getOverviewPanel()));
+ return getFromRecentsWindow(recentsWindowManager ->
+ (forTearDown && recentsWindowManager == null)
+ ? null : f.apply(recentsWindowManager.getOverviewPanel()));
} else {
- return getFromLauncher(launcher -> f.apply(launcher.getOverviewPanel()));
+ return getFromLauncher(launcher -> (forTearDown && launcher == null)
+ ? null : f.apply(launcher.getOverviewPanel()));
}
}
private void runOnRecentsView(Consumer<RecentsView> f) {
+ runOnRecentsView(f, false);
+ }
+
+ private void runOnRecentsView(Consumer<RecentsView> f, boolean forTearDown) {
getFromRecentsView(recentsView -> {
+ if (forTearDown && recentsView == null) {
+ return null;
+ }
f.accept(recentsView);
return null;
- });
+ }, forTearDown);
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt
index b4d9f5b..47108e0 100644
--- a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt
@@ -93,6 +93,21 @@
}
@Test
+ fun noLaunchTransition_returnsEmptyAnimatorsList() {
+ val pipChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_PIP
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(pipChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(0)
+ }
+
+ @Test
fun minimizeTransition_returnsLaunchAndMinimizeAnimator() {
val openChange =
TransitionInfo.Change(mock(), mock()).apply {
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index ad6afcf..a30261e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -39,6 +39,7 @@
import com.android.launcher3.util.SandboxContext
import com.android.launcher3.util.WindowBounds
import com.android.launcher3.util.rule.TestStabilityRule
+import com.android.launcher3.util.rule.ZipFilesRule
import com.android.launcher3.util.rule.setFlags
import com.android.launcher3.util.window.CachedDisplayInfo
import com.android.launcher3.util.window.WindowManagerProxy
@@ -51,6 +52,7 @@
import java.io.StringWriter
import kotlin.math.max
import kotlin.math.min
+import org.junit.ClassRule
import org.junit.Rule
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
@@ -80,6 +82,13 @@
@Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+ companion object {
+ @ClassRule
+ @JvmField
+ val resultZipRule =
+ ZipFilesRule(InstrumentationRegistry.getInstrumentation().targetContext, "DumpTest")
+ }
+
class DeviceSpec(
val naturalSize: Pair<Int, Int>,
var densityDpi: Int,
@@ -364,13 +373,9 @@
context.assets.open("dumpTests/$fileName").bufferedReader().use(BufferedReader::readText)
private fun writeToDevice(context: Context, fileName: String, content: String) {
- val dir =
- File(context.filesDir, "dumpTests").also {
- if (!it.exists()) {
- it.mkdirs()
- }
- }
- File(dir, fileName).writeText(content)
+ val file = File(context.getDir("dumpTests", Context.MODE_PRIVATE), fileName)
+ file.writeText(content)
+ resultZipRule.write(file)
}
protected fun Float.dpToPx(): Float {
diff --git a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
index 1e54603..38fad6b 100644
--- a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
+++ b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
@@ -32,7 +32,6 @@
import com.android.launcher3.util.rule.BackAndRestoreRule
import com.android.launcher3.util.rule.setFlags
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -70,7 +69,6 @@
}
@Test
- @Ignore("b/385147987")
fun oldDatabasesNotPresentAfterRestore() {
val dbController = ModelDbController(getInstrumentation().targetContext)
if (Flags.gridMigrationRefactor()) {
diff --git a/tests/src/com/android/launcher3/util/rule/ZipFilesRule.kt b/tests/src/com/android/launcher3/util/rule/ZipFilesRule.kt
new file mode 100644
index 0000000..d12de76
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/rule/ZipFilesRule.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.util.rule
+
+import android.content.Context
+import android.os.FileUtils
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+class ZipFilesRule(val context: Context, val name: String) : TestRule {
+
+ var resultsZip: ZipOutputStream? = null
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return object : Statement() {
+ @Throws(Throwable::class)
+ override fun evaluate() {
+ resultsZip =
+ ZipOutputStream(
+ FileOutputStream(
+ File(context.filesDir, "${description.testClass.simpleName}-$name.zip")
+ )
+ )
+ try {
+ base.evaluate() // This will run the test.
+ } finally {
+ resultsZip?.close()
+ }
+ }
+ }
+ }
+
+ fun write(file: File) {
+ if (resultsZip !is ZipOutputStream) {
+ throw RuntimeException(
+ "Cannot save files before the test rule starts! We need the rule to start to get the name of the test"
+ )
+ }
+ resultsZip!!.let {
+ it.putNextEntry(ZipEntry(file.name))
+ FileUtils.copy(FileInputStream(file), it)
+ it.closeEntry()
+ }
+ }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 8fbb5e3..02c6630 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -73,16 +73,18 @@
return getCombinedSplitTaskHeight();
}
- UiObject2 taskSnapshot1 = findObjectInTask((isDesktop() ? DESKTOP : DEFAULT).snapshotRes);
- return taskSnapshot1.getVisibleBounds().height();
+ if (isDesktop()) {
+ return getTaskSnapshot(DESKTOP).getVisibleBounds().height();
+ }
+ return getTaskSnapshot(DEFAULT).getVisibleBounds().height();
}
/**
* Calculates the visible height for split tasks, containing 2 snapshot tiles and a divider.
*/
private int getCombinedSplitTaskHeight() {
- UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes);
- UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes);
+ UiObject2 taskSnapshot1 = getTaskSnapshot(SPLIT_TOP_OR_LEFT);
+ UiObject2 taskSnapshot2 = getTaskSnapshot(SPLIT_BOTTOM_OR_RIGHT);
// If the split task is partly off screen, taskSnapshot1 can be invisible.
if (taskSnapshot1 == null) {
@@ -97,34 +99,6 @@
return bottom - top;
}
- /**
- * Returns the width of the visible task, or the combined width of two tasks in split with a
- * divider between.
- */
- int getVisibleWidth() {
- if (isGrouped()) {
- return getCombinedSplitTaskWidth();
- }
-
- UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes);
- return taskSnapshot1.getVisibleBounds().width();
- }
-
- /**
- * Calculates the visible width for split tasks, containing 2 snapshot tiles and a divider.
- */
- private int getCombinedSplitTaskWidth() {
- UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes);
- UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes);
-
- int left = Math.min(
- taskSnapshot1.getVisibleBounds().left, taskSnapshot2.getVisibleBounds().left);
- int right = Math.max(
- taskSnapshot1.getVisibleBounds().right, taskSnapshot2.getVisibleBounds().right);
-
- return right - left;
- }
-
public int getTaskCenterX() {
return mTask.getVisibleCenter().x;
}
@@ -142,6 +116,22 @@
}
/**
+ * Returns the task snapshot (thumbnail) for the given `OverviewTaskContainer`.
+ *
+ * For some reason `BySelector` does not work with `hasChild` or `hasParent` so instead we
+ * grab all the views matching the id: "snapshot" and filter for the correct parent.
+ */
+ private UiObject2 getTaskSnapshot(OverviewTaskContainer overviewTaskContainer) {
+ BySelector snapshotSelector = mLauncher.getOverviewObjectSelector("snapshot");
+ List<UiObject2> snapshots = mTask.findObjects(snapshotSelector);
+ return snapshots.stream()
+ .filter(snapshot -> snapshot.getParent().getResourceName()
+ .contains(overviewTaskContainer.taskContentViewRes))
+ .findFirst()
+ .orElse(snapshots.getFirst());
+ }
+
+ /**
* Dismisses the task by swiping up.
*/
public void dismiss() {
@@ -304,17 +294,13 @@
}
}
- private UiObject2 findObjectInTask(String resName) {
- return mTask.findObject(mLauncher.getOverviewObjectSelector(resName));
- }
-
/**
* Returns whether the given String is contained in this Task's contentDescription. Also returns
* true if both Strings are null.
*/
public boolean containsContentDescription(String expected,
OverviewTaskContainer overviewTaskContainer) {
- String actual = findObjectInTask(overviewTaskContainer.snapshotRes).getContentDescription();
+ String actual = getTaskSnapshot(overviewTaskContainer).getContentDescription();
if (actual == null && expected == null) {
return true;
}
@@ -360,19 +346,19 @@
*/
public enum OverviewTaskContainer {
// The main task when the task is not split.
- DEFAULT("snapshot", "icon"),
+ DEFAULT("task_content_view", "icon"),
// The first task in split task.
- SPLIT_TOP_OR_LEFT("snapshot", "icon"),
+ SPLIT_TOP_OR_LEFT("task_content_view", "icon"),
// The second task in split task.
- SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon"),
+ SPLIT_BOTTOM_OR_RIGHT("bottomright_task_content_view", "bottomRight_icon"),
// The desktop task.
DESKTOP("background", "icon");
- public final String snapshotRes;
+ public final String taskContentViewRes;
public final String iconAppRes;
- OverviewTaskContainer(String snapshotRes, String iconAppRes) {
- this.snapshotRes = snapshotRes;
+ OverviewTaskContainer(String taskContentViewRes, String iconAppRes) {
+ this.taskContentViewRes = taskContentViewRes;
this.iconAppRes = iconAppRes;
}
}