From 2244b633ccf40b8ce6a4e9daf1c869f4d40f91f2 Mon Sep 17 00:00:00 2001 From: samcackett Date: Fri, 21 Mar 2025 14:21:02 +0000 Subject: [PATCH] Add TaskContentView wrapper to TaskThumbnailView - Keep TaskThumbnailViewDeprecated as-is - Due to flag guarding and xml changes, added bottomright_snapshot id - Remove redundant FrameLayout from task_header_view.xml and refactor the view to handle alignment correctly - Move feature flag logic to the state mapper - Extract TaskThumbnailViewHeader logic and any related state out of TaskThumbnailView and move to TaskContentView - Use vertical LinearLayoutManager to hold the TaskThumbnailViewHeader and TaskThumbnailView - Rename TaskThumbnailViewHeader to TaskHeaderView. Rename xml and state similarly This reverts commit df6dc455a7cd5c4ec39b71b2552631b09e6e5a35. This reverts commit 714370a9bfb2e53738b0cd9958acd6540d8d649c. Reason for revert: Reland previously reverted CL's with fixes Fix: 408971730 Fix: 397889146 Fix: 401469907 Fix: 402277471 Fix: 403826044 Flag: com.android.launcher3.enable_refactor_task_thumbnail Test: TaskUiStateMapperTest & TaskContentViewScreenshotTest & TaskThumbnailViewScreenshotTest & TaskHeaderViewScreenshotTest & SwitchBetweenSplitPairsGesturalNavPortrait Change-Id: I01758447ad1194ebbeab748113621b42f3384db8 --- quickstep/res/layout/task.xml | 3 +- quickstep/res/layout/task_content_view.xml | 34 +++ quickstep/res/layout/task_grouped.xml | 6 +- quickstep/res/layout/task_header_view.xml | 62 +++++ quickstep/res/layout/task_thumbnail.xml | 3 +- .../res/layout/task_thumbnail_view_header.xml | 72 ------ quickstep/res/values/dimens.xml | 6 +- quickstep/res/values/ids.xml | 2 + .../orientation/LandscapePagedViewHandler.kt | 2 +- .../orientation/PortraitPagedViewHandler.kt | 3 +- .../orientation/SeascapePagedViewHandler.kt | 3 +- .../recents/ui/mapper/TaskUiStateMapper.kt | 88 +++---- .../task/thumbnail/TaskContentView.kt | 147 ++++++++++++ .../task/thumbnail/TaskHeaderUiState.kt | 32 +++ .../task/thumbnail/TaskThumbnailUiState.kt | 36 +-- .../task/thumbnail/TaskThumbnailView.kt | 66 ++--- .../util/SplitAnimationController.kt | 28 ++- .../quickstep/views/DesktopTaskView.kt | 59 +++-- .../quickstep/views/DigitalWellBeingToast.kt | 2 +- .../quickstep/views/GroupedTaskView.kt | 26 +- .../android/quickstep/views/TaskContainer.kt | 46 +++- ...umbnailViewHeader.kt => TaskHeaderView.kt} | 24 +- .../com/android/quickstep/views/TaskView.kt | 51 +++- .../quickstep/task/thumbnail/SplashHelper.kt | 43 ++++ .../TaskContentViewScreenshotTest.kt | 128 ++++++++++ .../thumbnail/TaskHeaderViewScreenshotTest.kt | 80 +++++++ .../TaskThumbnailViewScreenshotTest.kt | 71 +++--- .../model/data/TaskViewItemInfoTest.kt | 2 + .../ui/mapper/TaskUiStateMapperTest.kt | 225 ++++++++---------- .../AspectRatioSystemShortcutTests.kt | 7 + .../quickstep/DesktopSystemShortcutTest.kt | 7 + .../ExternalDisplaySystemShortcutTest.kt | 7 + .../android/launcher3/tapl/OverviewTask.java | 81 +++---- 33 files changed, 988 insertions(+), 464 deletions(-) create mode 100644 quickstep/res/layout/task_content_view.xml create mode 100644 quickstep/res/layout/task_header_view.xml delete mode 100644 quickstep/res/layout/task_thumbnail_view_header.xml create mode 100644 quickstep/src/com/android/quickstep/task/thumbnail/TaskContentView.kt create mode 100644 quickstep/src/com/android/quickstep/task/thumbnail/TaskHeaderUiState.kt rename quickstep/src/com/android/quickstep/views/{TaskThumbnailViewHeader.kt => TaskHeaderView.kt} (64%) create mode 100644 quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/SplashHelper.kt create mode 100644 quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskContentViewScreenshotTest.kt create mode 100644 quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskHeaderViewScreenshotTest.kt diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml index 1df80aeff2..bac7b26c32 100644 --- a/quickstep/res/layout/task.xml +++ b/quickstep/res/layout/task.xml @@ -29,8 +29,7 @@ launcher:hoverBorderColor="@color/materialColorPrimary"> diff --git a/quickstep/res/layout/task_content_view.xml b/quickstep/res/layout/task_content_view.xml new file mode 100644 index 0000000000..478ee55c06 --- /dev/null +++ b/quickstep/res/layout/task_content_view.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml index fe6ada98df..41f892f7fc 100644 --- a/quickstep/res/layout/task_grouped.xml +++ b/quickstep/res/layout/task_grouped.xml @@ -34,14 +34,12 @@ launcher:hoverBorderColor="@color/materialColorPrimary"> diff --git a/quickstep/res/layout/task_header_view.xml b/quickstep/res/layout/task_header_view.xml new file mode 100644 index 0000000000..849153fd9e --- /dev/null +++ b/quickstep/res/layout/task_header_view.xml @@ -0,0 +1,62 @@ + + + + + + diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml index 3b966159d1..8280e13f5b 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" > - - - - - - - - diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 75bf331e0e..3b0be2f389 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -86,9 +86,9 @@ 20dp - 30dp - 18dp - 9dp + 6dp + 12dp + 8dp 18dp 16dp diff --git a/quickstep/res/values/ids.xml b/quickstep/res/values/ids.xml index c71bb762db..4cc69ade5a 100644 --- a/quickstep/res/values/ids.xml +++ b/quickstep/res/values/ids.xml @@ -21,4 +21,6 @@ + + \ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt index 1e626cd78d..4868369de9 100644 --- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt @@ -277,7 +277,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { desiredTaskId: Int, banner: View, ): Pair { - val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams + val snapshotParams = thumbnailViews[0].layoutParams as ViewGroup.MarginLayoutParams 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 9132783b60..58ef09ed7d 100644 --- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt @@ -257,7 +257,8 @@ class PortraitPagedViewHandler : DefaultPagedViewHandler(), RecentsPagedOrientat } } else { if (desiredTaskId == splitBounds.leftTopTaskId) { - val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams + val snapshotParams = + thumbnailViews[0].layoutParams as ViewGroup.MarginLayoutParams 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 dcd7d876e3..35103219a0 100644 --- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt @@ -25,6 +25,7 @@ import android.view.Gravity import android.view.Surface import android.view.View import android.view.View.MeasureSpec +import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.util.component1 import androidx.core.util.component2 @@ -151,7 +152,7 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { desiredTaskId: Int, banner: View, ): Pair { - val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams + val snapshotParams = thumbnailViews[0].layoutParams as ViewGroup.MarginLayoutParams 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 3f6ea0f386..7425d361ee 100644 --- a/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt +++ b/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt @@ -17,17 +17,50 @@ package com.android.quickstep.recents.ui.mapper import android.view.View.OnClickListener +import com.android.launcher3.Flags.enableDesktopExplodedView 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. * @@ -36,45 +69,24 @@ object TaskUiStateMapper { * * @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?, - hasHeader: Boolean, - clickCloseListener: OnClickListener?, - ): TaskThumbnailUiState = + fun toTaskThumbnailUiState(taskData: TaskData?): TaskThumbnailUiState = when { taskData !is TaskData.Data -> Uninitialized - taskData.isLiveTile -> createLiveTileState(taskData, hasHeader, clickCloseListener) + taskData.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) = @@ -88,21 +100,9 @@ object TaskUiStateMapper { hasHeader: Boolean, clickCloseListener: OnClickListener?, ) = - hasHeader && + enableDesktopExplodedView() && + hasHeader && 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 0000000000..a010f81c1f --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskContentView.kt @@ -0,0 +1,147 @@ +/* + * 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.View +import android.view.ViewOutlineProvider +import android.view.ViewStub +import android.widget.LinearLayout +import androidx.core.view.isInvisible +import com.android.launcher3.R +import com.android.launcher3.util.ViewPool +import com.android.quickstep.views.TaskHeaderView + +/** + * 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 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() + 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 + alpha = 1.0f + taskThumbnailView?.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 createHeaderView(taskHeaderState: TaskHeaderUiState) { + if (taskHeaderView == null && taskHeaderState is TaskHeaderUiState.ShowHeader) { + taskHeaderView = + findViewById(R.id.task_header_view) + .apply { layoutResource = R.layout.task_header_view } + .inflate() as TaskHeaderView + } + } + + private fun createTaskThumbnailView() { + if (taskThumbnailView == null) { + taskThumbnailView = + findViewById(R.id.snapshot) + .apply { layoutResource = R.layout.task_thumbnail } + .inflate() as TaskThumbnailView + } + } + + fun setState( + taskHeaderState: TaskHeaderUiState, + taskThumbnailUiState: TaskThumbnailUiState, + taskId: Int?, + ) { + createHeaderView(taskHeaderState) + 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 0000000000..09fb5409b5 --- /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 db593d34d3..a5c9ac032f 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 @@ package com.android.quickstep.task.thumbnail 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 @@ sealed class TaskThumbnailUiState { 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 52e2fcdf6c..83d6025d8e 100644 --- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt +++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt @@ -25,13 +25,12 @@ 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.Flags.enableRefactorTaskContentView import com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA import com.android.launcher3.R import com.android.launcher3.util.MultiPropertyFactory @@ -42,7 +41,6 @@ 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 { private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) } @@ -56,8 +54,6 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable { private val outlinePath = Path() private var onSizeChanged: ((width: Int, height: Int) -> Unit)? = null - private var taskThumbnailViewHeader: TaskThumbnailViewHeader? = null - private var uiState: TaskThumbnailUiState = Uninitialized /** @@ -89,6 +85,9 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable { override fun onAttachedToWindow() { super.onAttachedToWindow() + if (enableRefactorTaskContentView()) { + return + } clipToOutline = true outlineProvider = object : ViewOutlineProvider() { @@ -113,9 +112,10 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable { override fun onRecycle() { uiState = Uninitialized - onSizeChanged = null - outlineBounds = null - alpha = 1.0f + if (!enableRefactorTaskContentView()) { + onSizeChanged = null + outlineBounds = null + } resetViews() } @@ -126,7 +126,7 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable { resetViews() when (state) { is Uninitialized -> {} - is LiveTile -> drawLiveWindow(state) + is LiveTile -> drawLiveWindow() is SnapshotSplash -> drawSnapshotSplash(state) is BackgroundOnly -> drawBackground(state.backgroundColor) } @@ -152,10 +152,16 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable { } fun doOnSizeChange(action: (width: Int, height: Int) -> Unit) { + if (enableRefactorTaskContentView()) { + return + } onSizeChanged = action } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + if (enableRefactorTaskContentView()) { + return + } super.onSizeChanged(w, h, oldw, oldh) onSizeChanged?.invoke(width, height) bounds.set(0, 0, w, h) @@ -163,17 +169,33 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable { } override fun setScaleX(scaleX: Float) { + if (enableRefactorTaskContentView()) { + return + } super.setScaleX(scaleX) // Splash icon should ignore scale on TTV splashIcon.scaleX = 1 / scaleX } override fun setScaleY(scaleY: Float) { + if (enableRefactorTaskContentView()) { + return + } super.setScaleY(scaleY) // Splash icon should ignore scale on TTV splashIcon.scaleY = 1 / scaleY } + fun parentScaleXUpdated(scaleX: Float) { + // Splash icon should ignore scale on TTV + splashIcon.scaleX = 1 / scaleX + } + + fun parentScaleYUpdated(scaleY: Float) { + // Splash icon should ignore scale on TTV + splashIcon.scaleY = 1 / scaleY + } + private fun resetViews() { liveTileView.isInvisible = true thumbnailView.isInvisible = true @@ -182,22 +204,16 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable { splashIcon.alpha = 0f splashIcon.setImageDrawable(null) scrimView.alpha = 0f + alpha = 1.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) { - maybeCreateHeader() - taskThumbnailViewHeader?.isInvisible = false - taskThumbnailViewHeader?.setHeader(liveTile.header) - } } private fun drawSnapshotSplash(snapshotSplash: SnapshotSplash) { @@ -209,12 +225,6 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable { } private fun drawSnapshot(snapshot: Snapshot) { - if (snapshot is Snapshot.WithHeader) { - maybeCreateHeader() - taskThumbnailViewHeader?.isInvisible = false - taskThumbnailViewHeader?.setHeader(snapshot.header) - } - drawBackground(snapshot.backgroundColor) thumbnailView.setImageBitmap(snapshot.bitmap) thumbnailView.isInvisible = false @@ -230,16 +240,6 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable { 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/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt index 78e2b1a720..a96707eefd 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -193,7 +193,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC taskViewHeight: Int, isPrimaryTaskSplitting: Boolean, ) { - val snapshot = taskContainer.snapshotView + val taskContentView = taskContainer.taskContentView val iconView: View = taskContainer.iconView.asView() if (enableRefactorTaskThumbnail()) { builder.add( @@ -242,7 +242,11 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC 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 @@ -251,15 +255,17 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC 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 @@ -282,18 +288,22 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC } 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 38c0eb3cc9..1f719480f8 100644 --- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt @@ -33,6 +33,7 @@ import androidx.core.content.res.ResourcesCompat import androidx.core.view.updateLayoutParams import com.android.internal.hidden_from_bootclasspath.com.android.window.flags.Flags.enableDesktopRecentsTransitionsCornersBugfix import com.android.launcher3.Flags.enableDesktopExplodedView +import com.android.launcher3.Flags.enableRefactorTaskContentView import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.R import com.android.launcher3.statehandlers.DesktopVisibilityController @@ -56,6 +57,7 @@ import com.android.quickstep.recents.di.get 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 @@ -100,6 +102,17 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu ) } else null + private val taskContentViewPool = + if (enableRefactorTaskContentView()) { + ViewPool( + context, + this, + R.layout.task_content_view, + VIEW_POOL_MAX_SIZE, + VIEW_POOL_INITIAL_SIZE, + ) + } else null + private val tempPointF = PointF() private val lastComputedTaskSize = Rect() private lateinit var iconView: TaskViewIcon @@ -241,7 +254,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu val overviewTaskHeight = overviewTaskBounds.height() * scaleHeight if (updateLayout) { // Position the task to the same position as it would be on the desktop - taskContainer.snapshotView.updateLayoutParams { + taskContainer.taskContentView.updateLayoutParams { gravity = Gravity.LEFT or Gravity.TOP width = overviewTaskWidth.toInt() height = overviewTaskHeight.toInt() @@ -257,7 +270,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu overviewTaskBounds.width().toFloat() / currentTaskBounds.width() val thumbnailScaleHeight = overviewTaskBounds.height().toFloat() / currentTaskBounds.height() - taskContainer.thumbnailView.outlineBounds = + val contentOutlineBounds = if (intersects(currentTaskBounds, screenRect)) Rect(currentTaskBounds).apply { intersectUnchecked(screenRect) @@ -270,6 +283,13 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu bottom = (bottom * scaleHeight * thumbnailScaleHeight).roundToInt() } else null + + if (enableRefactorTaskContentView()) { + (taskContainer.taskContentView as TaskContentView).outlineBounds = + contentOutlineBounds + } else { + taskContainer.thumbnailView.outlineBounds = contentOutlineBounds + } } val currentTaskLeft = currentTaskBounds.left * scaleWidth @@ -278,7 +298,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu val currentTaskHeight = currentTaskBounds.height() * scaleHeight // During the animation, apply translation and scale such that the view is transformed // to where we want, without triggering layout. - taskContainer.snapshotView.apply { + taskContainer.taskContentView.apply { pivotX = 0.0f pivotY = 0.0f translationX = currentTaskLeft - overviewTaskLeft @@ -288,7 +308,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu } if (taskContainer.task.isMinimized) { - taskContainer.snapshotView.alpha = explodeProgress + taskContainer.taskContentView.alpha = explodeProgress } } } @@ -331,17 +351,24 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu val backgroundViewIndex = contentView.indexOfChild(backgroundView) taskContainers = tasks.map { task -> - val snapshotView = - if (enableRefactorTaskThumbnail()) { - taskThumbnailViewPool!!.view - } else { - taskThumbnailViewDeprecatedPool!!.view + val taskContentView = + when { + enableRefactorTaskContentView() -> taskContentViewPool!!.view + enableRefactorTaskThumbnail() -> taskThumbnailViewPool!!.view + else -> taskThumbnailViewDeprecatedPool!!.view + } + contentView.addView(taskContentView, backgroundViewIndex + 1) + val snapshotView = + if (enableRefactorTaskContentView()) { + taskContentView.findViewById(R.id.snapshot) + } else { + taskContentView } - contentView.addView(snapshotView, backgroundViewIndex + 1) TaskContainer( this, task, + taskContentView, snapshotView, iconView, TransformingTouchDelegate(iconView.asView()), @@ -507,11 +534,13 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu } private fun removeAndRecycleThumbnailView(taskContainer: TaskContainer) { - contentView.removeView(taskContainer.snapshotView) - if (enableRefactorTaskThumbnail()) { - taskThumbnailViewPool!!.recycle(taskContainer.thumbnailView) - } else { - taskThumbnailViewDeprecatedPool!!.recycle(taskContainer.thumbnailViewDeprecated) + contentView.removeView(taskContainer.taskContentView) + when { + enableRefactorTaskContentView() -> + taskContentViewPool!!.recycle(taskContainer.taskContentView as TaskContentView) + enableRefactorTaskThumbnail() -> + taskThumbnailViewPool!!.recycle(taskContainer.taskContentView as TaskThumbnailView) + else -> taskThumbnailViewDeprecatedPool!!.recycle(taskContainer.thumbnailViewDeprecated) } } diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt index 39739a1755..f72e351ece 100644 --- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt +++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt @@ -339,7 +339,7 @@ constructor( taskView.layoutParams.height, splitBounds, recentsViewContainer.deviceProfile, - taskView.snapshotViews, + taskView.taskContentViews, task.key.id, this, ) diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt index e3e987ccf7..34abcc084b 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt @@ -23,6 +23,7 @@ import android.util.Log import android.view.View import android.view.ViewStub import com.android.internal.jank.Cuj +import com.android.launcher3.Flags.enableRefactorTaskContentView import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.R import com.android.launcher3.Utilities @@ -78,8 +79,8 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu val splitBoundsConfig = splitBoundsConfig ?: return val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds( - leftTopTaskContainer.snapshotView, - rightBottomTaskContainer.snapshotView, + leftTopTaskContainer.taskContentView, + rightBottomTaskContainer.taskContentView, widthSize, heightSize, splitBoundsConfig, @@ -95,11 +96,17 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu override fun inflateViewStubs() { super.inflateViewStubs() - findViewById(R.id.bottomright_snapshot) + findViewById(R.id.bottomright_task_content_view) ?.apply { + inflatedId = + if (enableRefactorTaskContentView()) R.id.bottomright_task_content_view + else R.id.bottomright_snapshot layoutResource = - if (enableRefactorTaskThumbnail()) R.layout.task_thumbnail - else R.layout.task_thumbnail_deprecated + when { + enableRefactorTaskContentView() -> R.layout.task_content_view + enableRefactorTaskThumbnail() -> R.layout.task_thumbnail + else -> R.layout.task_thumbnail_deprecated + } } ?.inflate() findViewById(R.id.bottomRight_icon) @@ -130,6 +137,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu listOf( createTaskContainer( splitTask.topLeftTask, + R.id.task_content_view, R.id.snapshot, R.id.icon, R.id.show_windows, @@ -139,7 +147,9 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu ), createTaskContainer( splitTask.bottomRightTask, - R.id.bottomright_snapshot, + R.id.bottomright_task_content_view, + if (enableRefactorTaskContentView()) R.id.snapshot + else R.id.bottomright_snapshot, R.id.bottomRight_icon, R.id.show_windows_right, R.id.bottomRight_digital_wellbeing_toast, @@ -242,8 +252,8 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu leftTopTaskContainer.iconView.asView(), rightBottomTaskContainer.iconView.asView(), taskIconHeight, - leftTopTaskContainer.snapshotView.measuredWidth, - leftTopTaskContainer.snapshotView.measuredHeight, + leftTopTaskContainer.taskContentView.measuredWidth, + leftTopTaskContainer.taskContentView.measuredHeight, measuredHeight, measuredWidth, isLayoutRtl, diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt index 919907bd01..b187df1e99 100644 --- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt +++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt @@ -21,6 +21,7 @@ import android.graphics.Matrix import android.view.View import android.view.View.OnClickListener import com.android.app.tracing.traceSection +import com.android.launcher3.Flags.enableRefactorTaskContentView import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.model.data.TaskViewItemInfo import com.android.launcher3.util.OverviewReleaseFlags.enableOverviewIconMenu @@ -31,6 +32,7 @@ import com.android.quickstep.ViewUtils.addAccessibleChildToList import com.android.quickstep.recents.domain.usecase.ThumbnailPosition import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper import com.android.quickstep.recents.ui.viewmodel.TaskData +import com.android.quickstep.task.thumbnail.TaskContentView import com.android.quickstep.task.thumbnail.TaskThumbnailView import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData @@ -39,6 +41,8 @@ import com.android.systemui.shared.recents.model.ThumbnailData class TaskContainer( val taskView: TaskView, val task: Task, + // TODO(b/409248525): Upon flag cleanup, use the `TaskContentView` type + val taskContentView: View, val snapshotView: View, val iconView: TaskViewIcon, /** @@ -59,10 +63,19 @@ class TaskContainer( private var overlayEnabledStatus = false init { - if (enableRefactorTaskThumbnail()) { - require(snapshotView is TaskThumbnailView) - } else { - require(snapshotView is TaskThumbnailViewDeprecated) + when { + enableRefactorTaskContentView() -> { + require(taskContentView is TaskContentView) + require(snapshotView is TaskThumbnailView) + } + enableRefactorTaskThumbnail() -> { + require(taskContentView is TaskThumbnailView) + require(snapshotView is TaskThumbnailView) + } + else -> { + require(taskContentView is TaskThumbnailViewDeprecated) + require(snapshotView is TaskThumbnailViewDeprecated) + } } } @@ -110,8 +123,8 @@ class TaskContainer( fun destroy() = traceSection("TaskContainer.destroy") { digitalWellBeingToast?.destroy() - snapshotView.scaleX = 1f - snapshotView.scaleY = 1f + taskContentView.scaleX = 1f + taskContentView.scaleY = 1f overlay.reset() if (enableRefactorTaskThumbnail()) { isThumbnailValid = false @@ -159,7 +172,10 @@ class TaskContainer( fun addChildForAccessibility(outChildren: ArrayList) { addAccessibleChildToList(iconView.asView(), outChildren) - addAccessibleChildToList(snapshotView, outChildren) + addAccessibleChildToList( + if (enableRefactorTaskContentView()) taskContentView else snapshotView, + outChildren, + ) showWindowsView?.let { addAccessibleChildToList(it, outChildren) } digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) } overlay.addChildForAccessibility(outChildren) @@ -167,10 +183,18 @@ class TaskContainer( fun setState(state: TaskData?, hasHeader: Boolean, clickCloseListener: OnClickListener?) = traceSection("TaskContainer.setState") { - thumbnailView.setState( - TaskUiStateMapper.toTaskThumbnailUiState(state, hasHeader, clickCloseListener), - state?.taskId, - ) + if (enableRefactorTaskContentView()) { + (taskContentView as TaskContentView).setState( + TaskUiStateMapper.toTaskHeaderState(state, hasHeader, clickCloseListener), + TaskUiStateMapper.toTaskThumbnailUiState(state), + state?.taskId, + ) + } else { + thumbnailView.setState( + TaskUiStateMapper.toTaskThumbnailUiState(state), + state?.taskId, + ) + } thumbnailData = if (state is TaskData.Data) state.thumbnailData else null overlay.setThumbnailState(thumbnailData) } diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt b/quickstep/src/com/android/quickstep/views/TaskHeaderView.kt similarity index 64% rename from quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt rename to quickstep/src/com/android/quickstep/views/TaskHeaderView.kt index 9a8805bf0f..1fda5a3ace 100644 --- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt +++ b/quickstep/src/com/android/quickstep/views/TaskHeaderView.kt @@ -18,23 +18,33 @@ package com.android.quickstep.views import android.content.Context import android.util.AttributeSet -import android.widget.FrameLayout import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +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) : + ConstraintLayout(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 0edd87bfe3..49dd66805c 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.kt +++ b/quickstep/src/com/android/quickstep/views/TaskView.kt @@ -48,6 +48,7 @@ import com.android.launcher3.Flags.enableCursorHoverStates import com.android.launcher3.Flags.enableDesktopExplodedView import com.android.launcher3.Flags.enableHoverOfChildElementsInTaskview import com.android.launcher3.Flags.enableLargeDesktopWindowingTile +import com.android.launcher3.Flags.enableRefactorTaskContentView import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks import com.android.launcher3.R @@ -88,6 +89,7 @@ import com.android.quickstep.recents.domain.usecase.ThumbnailPosition import com.android.quickstep.recents.ui.viewmodel.TaskData import com.android.quickstep.recents.ui.viewmodel.TaskTileUiState import com.android.quickstep.recents.ui.viewmodel.TaskViewModel +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 @@ -146,6 +148,9 @@ constructor( val snapshotViews: Array get() = taskContainers.map { it.snapshotView }.toTypedArray() + val taskContentViews: Array + get() = taskContainers.map { it.taskContentView }.toTypedArray() + val isGridTask: Boolean /** Returns whether the task is part of overview grid and not being focused. */ get() = container.deviceProfile.isTablet && !isLargeTile @@ -785,13 +790,19 @@ constructor( } protected open fun inflateViewStubs() { - findViewById(R.id.snapshot) + findViewById(R.id.task_content_view) ?.apply { + inflatedId = + if (enableRefactorTaskContentView()) R.id.task_content_view else R.id.snapshot layoutResource = - if (enableRefactorTaskThumbnail()) R.layout.task_thumbnail - else R.layout.task_thumbnail_deprecated + when { + enableRefactorTaskContentView() -> R.layout.task_content_view + enableRefactorTaskThumbnail() -> R.layout.task_thumbnail + else -> R.layout.task_thumbnail_deprecated + } } ?.inflate() + findViewById(R.id.icon) ?.apply { layoutResource = @@ -950,6 +961,7 @@ constructor( listOf( createTaskContainer( singleTask.task, + R.id.task_content_view, R.id.snapshot, R.id.icon, R.id.show_windows, @@ -972,7 +984,15 @@ constructor( taskContainers.forEach { container -> container.bind() - if (enableRefactorTaskThumbnail()) { + if (enableRefactorTaskContentView()) { + (container.taskContentView as TaskContentView).cornerRadius = + thumbnailFullscreenParams.currentCornerRadius + container.taskContentView.doOnSizeChange { width, height -> + updateThumbnailValidity(container) + val thumbnailPosition = updateThumbnailMatrix(container, width, height) + container.refreshOverlay(thumbnailPosition) + } + } else if (enableRefactorTaskThumbnail()) { container.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius container.thumbnailView.doOnSizeChange { width, height -> @@ -1002,6 +1022,7 @@ constructor( protected fun createTaskContainer( task: Task, + @IdRes taskContentViewId: Int, @IdRes thumbnailViewId: Int, @IdRes iconViewId: Int, @IdRes showWindowViewId: Int, @@ -1011,10 +1032,17 @@ constructor( ): TaskContainer = traceSection("TaskView.createTaskContainer") { val iconView = findViewById(iconViewId) as TaskViewIcon + val taskContentView = + if (enableRefactorTaskContentView()) findViewById(taskContentViewId) + else findViewById(thumbnailViewId) + val snapshotView = + if (enableRefactorTaskContentView()) taskContentView.findViewById(thumbnailViewId) + else taskContentView return TaskContainer( this, task, - findViewById(thumbnailViewId), + taskContentView, + snapshotView, iconView, TransformingTouchDelegate(iconView.asView()), stagePosition, @@ -1103,7 +1131,7 @@ constructor( 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 { + firstTaskContainer?.taskContentView?.updateLayoutParams { topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx } taskContainers.forEach { it.digitalWellBeingToast?.setupLayout() } @@ -1117,11 +1145,11 @@ constructor( 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) } @@ -1762,7 +1790,7 @@ constructor( open fun setThumbnailVisibility(visibility: Int, taskId: Int) { taskContainers.forEach { if (visibility == VISIBLE || it.task.key.id == taskId) { - it.snapshotView.visibility = visibility + it.taskContentView.visibility = visibility it.digitalWellBeingToast?.visibility = visibility it.showWindowsView?.visibility = visibility it.overlay.setVisibility(visibility) @@ -1836,7 +1864,10 @@ constructor( protected open fun updateFullscreenParams() { updateFullscreenParams(thumbnailFullscreenParams) taskContainers.forEach { - if (enableRefactorTaskThumbnail()) { + if (enableRefactorTaskContentView()) { + (it.taskContentView as TaskContentView).cornerRadius = + thumbnailFullscreenParams.currentCornerRadius + } else if (enableRefactorTaskThumbnail()) { it.thumbnailView.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 0000000000..8cc09d470b --- /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 0000000000..5e546f7f1a --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskContentViewScreenshotTest.kt @@ -0,0 +1,128 @@ +/* + * 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.platform.test.flag.junit.SetFlagsRule +import android.view.LayoutInflater +import com.android.launcher3.Flags +import com.android.launcher3.R +import com.android.launcher3.util.rule.setFlags +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.Before +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(order = 0) val setFlagsRule = SetFlagsRule() + + @get:Rule(order = 1) + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Before + fun setUp() { + setFlagsRule.setFlags( + true, + Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL, + Flags.FLAG_ENABLE_REFACTOR_TASK_CONTENT_VIEW, + ) + } + + @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 0000000000..e30554e647 --- /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 80b2c16e10..81b046adb3 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,21 +16,23 @@ 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.platform.test.flag.junit.SetFlagsRule import android.view.LayoutInflater import android.view.Surface.ROTATION_0 -import androidx.core.graphics.set +import com.android.launcher3.Flags import com.android.launcher3.R +import com.android.launcher3.util.rule.setFlags +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 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -45,13 +47,20 @@ import platform.test.screenshot.getEmulatedDevicePathConfig @RunWith(ParameterizedAndroidJunit4::class) class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { - @get:Rule + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + + @get:Rule(order = 1) val screenshotRule = ViewScreenshotTestRule( emulationSpec, ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), ) + @Before + fun setUp() { + setFlagsRule.setFlags(false, Flags.FLAG_ENABLE_REFACTOR_TASK_CONTENT_VIEW) + } + @Test fun taskThumbnailView_uninitializedByDefault() { screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity -> @@ -90,23 +99,25 @@ class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { } @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 +126,14 @@ class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { } @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 +150,17 @@ class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { } @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 +170,14 @@ class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { } @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, @@ -233,27 +248,6 @@ class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { 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 @@ -265,7 +259,6 @@ class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { ) 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 6170551420..61c0a1b54b 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 @@ -36,6 +36,7 @@ import com.android.launcher3.util.UserIconInfo 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 @@ -201,6 +202,7 @@ class TaskViewItemInfoTest { return TaskContainer( taskView, task, + mock(), if (enableRefactorTaskThumbnail()) mock() else mock(), mock(), 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 cc658c7afc..b7c86ce900 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 @@ -19,32 +19,120 @@ package com.android.quickstep.recents.ui.mapper import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.ShapeDrawable +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.view.Surface import android.view.View 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.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class TaskUiStateMapperTest { + @get:Rule val mSetFlagsRule = SetFlagsRule() + + /** TaskHeaderUiState */ @Test - fun taskData_isNull_returns_Uninitialized() { + fun taskData_isNull_returns_HideHeader() { val result = - TaskUiStateMapper.toTaskThumbnailUiState( + TaskUiStateMapper.toTaskHeaderState( taskData = null, hasHeader = false, clickCloseListener = null, ) + assertThat(result).isEqualTo(TaskHeaderUiState.HideHeader) + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) + @Test + fun explodedFlagDisabled_returnsHideHeader() { + val inputs = + listOf( + TASK_DATA, + TASK_DATA.copy(thumbnailData = null), + TASK_DATA.copy(isLocked = true), + TASK_DATA.copy(title = null), + ) + val closeCallback = View.OnClickListener {} + val expected = TaskHeaderUiState.HideHeader + inputs.forEach { taskData -> + val result = + TaskUiStateMapper.toTaskHeaderState( + taskData = taskData, + hasHeader = true, + clickCloseListener = closeCallback, + ) + assertThat(result).isEqualTo(expected) + } + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) + @Test + fun taskData_hasHeader_and_taskData_returnsShowHeader() { + val inputs = + listOf( + TASK_DATA.copy(isLiveTile = true), + TASK_DATA.copy(isLiveTile = true, thumbnailData = null), + TASK_DATA.copy(isLiveTile = true, isLocked = true), + TASK_DATA.copy(isLiveTile = true, title = null), + ) + val closeCallback = View.OnClickListener {} + val expected = + TaskHeaderUiState.ShowHeader( + header = + TaskHeaderUiState.ThumbnailHeader( + TASK_ICON, + TASK_TITLE_DESCRIPTION, + closeCallback, + ) + ) + inputs.forEach { taskData -> + val result = + TaskUiStateMapper.toTaskHeaderState( + taskData = taskData, + hasHeader = true, + clickCloseListener = closeCallback, + ) + assertThat(result).isEqualTo(expected) + } + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) + @Test + fun taskData_hasHeader_emptyTaskData_returns_HideHeader() { + val inputs = + listOf( + TASK_DATA.copy(isLiveTile = true, icon = null), + TASK_DATA.copy(isLiveTile = true, titleDescription = null), + TASK_DATA.copy(isLiveTile = true, icon = null, titleDescription = null), + ) + + inputs.forEach { taskData -> + val result = + TaskUiStateMapper.toTaskHeaderState( + taskData = taskData, + hasHeader = true, + clickCloseListener = {}, + ) + assertThat(result).isEqualTo(TaskHeaderUiState.HideHeader) + } + } + + /** TaskThumbnailUiState */ + @Test + fun taskData_isNull_returns_Uninitialized() { + val result = TaskUiStateMapper.toTaskThumbnailUiState(taskData = null) assertThat(result).isEqualTo(TaskThumbnailUiState.Uninitialized) } @@ -57,76 +145,19 @@ class TaskUiStateMapperTest { TASK_DATA.copy(isLiveTile = true, isLocked = true), ) inputs.forEach { input -> - val result = - TaskUiStateMapper.toTaskThumbnailUiState( - taskData = input, - hasHeader = false, - clickCloseListener = null, - ) - assertThat(result).isEqualTo(LiveTile.WithoutHeader) - } - } - - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) - @Test - fun taskData_isLiveTileWithHeader_returns_LiveTileWithHeader() { - val inputs = - listOf( - TASK_DATA.copy(isLiveTile = true), - TASK_DATA.copy(isLiveTile = true, thumbnailData = null), - TASK_DATA.copy(isLiveTile = true, isLocked = true), - TASK_DATA.copy(isLiveTile = true, title = null), - ) - val closeCallback = View.OnClickListener {} - val expected = - LiveTile.WithHeader( - header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION, closeCallback) - ) - inputs.forEach { taskData -> - val result = - TaskUiStateMapper.toTaskThumbnailUiState( - taskData = taskData, - hasHeader = true, - clickCloseListener = closeCallback, - ) - assertThat(result).isEqualTo(expected) - } - } - - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) - @Test - fun taskData_isLiveTileWithHeader_missingHeaderData_returns_LiveTileWithoutHeader() { - val inputs = - listOf( - TASK_DATA.copy(isLiveTile = true, icon = null), - TASK_DATA.copy(isLiveTile = true, titleDescription = null), - TASK_DATA.copy(isLiveTile = true, icon = null, titleDescription = null), - ) - - inputs.forEach { taskData -> - val result = - TaskUiStateMapper.toTaskThumbnailUiState( - taskData = taskData, - hasHeader = true, - clickCloseListener = {}, - ) - assertThat(result).isEqualTo(LiveTile.WithoutHeader) + val result = TaskUiStateMapper.toTaskThumbnailUiState(taskData = input) + assertThat(result).isEqualTo(LiveTile) } } @Test fun taskData_isStaticTile_returns_SnapshotSplash() { - val result = - TaskUiStateMapper.toTaskThumbnailUiState( - taskData = TASK_DATA, - hasHeader = false, - clickCloseListener = null, - ) + val result = TaskUiStateMapper.toTaskThumbnailUiState(taskData = TASK_DATA) val expected = TaskThumbnailUiState.SnapshotSplash( snapshot = - Snapshot.WithoutHeader( + Snapshot( backgroundColor = TASK_BACKGROUND_COLOR, bitmap = TASK_THUMBNAIL, thumbnailRotation = Surface.ROTATION_0, @@ -137,69 +168,11 @@ class TaskUiStateMapperTest { 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, - 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, - 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), - hasHeader = false, - clickCloseListener = null, + taskData = TASK_DATA.copy(thumbnailData = null) ) val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR) @@ -209,11 +182,7 @@ class TaskUiStateMapperTest { @Test fun taskData_isLocked_returns_BackgroundOnly() { val result = - TaskUiStateMapper.toTaskThumbnailUiState( - taskData = TASK_DATA.copy(isLocked = true), - hasHeader = false, - clickCloseListener = null, - ) + TaskUiStateMapper.toTaskThumbnailUiState(taskData = TASK_DATA.copy(isLocked = true)) val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR) assertThat(result).isEqualTo(expected) diff --git a/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt b/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt index 1e7c116f94..fa245da495 100644 --- a/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt +++ b/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt @@ -33,6 +33,7 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.AbstractFloatingView import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags.enableRefactorTaskContentView import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.InvariantDeviceProfile import com.android.launcher3.R @@ -50,6 +51,7 @@ import com.android.quickstep.TaskViewTestDIHelpers.initializeRecentsDependencies import com.android.quickstep.TaskViewTestDIHelpers.mockRecentsModel import com.android.quickstep.orientation.LandscapePagedViewHandler 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.util.RecentsOrientedState import com.android.quickstep.util.SingleTask @@ -267,6 +269,11 @@ class AspectRatioSystemShortcutTests { TaskContainer( taskView, task, + when { + enableRefactorTaskContentView() -> mock() + enableRefactorTaskThumbnail() -> mock() + else -> mock() + }, if (enableRefactorTaskThumbnail()) mock() else mock(), mock(), diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt index 8b0c3d9821..b9392531f0 100644 --- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt +++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt @@ -29,6 +29,7 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.internal.R import com.android.launcher3.AbstractFloatingView import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags.enableRefactorTaskContentView import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.logging.StatsLogManager import com.android.launcher3.logging.StatsLogManager.LauncherEvent @@ -36,6 +37,7 @@ import com.android.launcher3.model.data.TaskViewItemInfo 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 @@ -274,6 +276,11 @@ class DesktopSystemShortcutTest { TaskContainer( taskView, task, + when { + enableRefactorTaskContentView() -> mock() + enableRefactorTaskThumbnail() -> mock() + else -> mock() + }, if (enableRefactorTaskThumbnail()) mock() else mock(), mock(), diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt index 48b2ae3d3c..9b384318df 100644 --- a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt +++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt @@ -29,6 +29,7 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.internal.R import com.android.launcher3.AbstractFloatingView import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags.enableRefactorTaskContentView import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.logging.StatsLogManager import com.android.launcher3.logging.StatsLogManager.LauncherEvent @@ -36,6 +37,7 @@ import com.android.launcher3.model.data.TaskViewItemInfo 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 @@ -270,6 +272,11 @@ class ExternalDisplaySystemShortcutTest { TaskContainer( taskView, task, + when { + enableRefactorTaskContentView() -> mock() + enableRefactorTaskThumbnail() -> mock() + else -> mock() + }, if (enableRefactorTaskThumbnail()) mock() else mock(), mock(), diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java index fe92cbfb76..d27e9a03de 100644 --- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java +++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java @@ -73,16 +73,15 @@ public final class OverviewTask { return getCombinedSplitTaskHeight(); } - UiObject2 taskSnapshot1 = findObjectInTask((isDesktop() ? DESKTOP : DEFAULT).snapshotRes); - return taskSnapshot1.getVisibleBounds().height(); + return getTaskSnapshot(isDesktop() ? DESKTOP : 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 +96,6 @@ public final class OverviewTask { 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; } @@ -141,6 +112,26 @@ public final class OverviewTask { return mTask; } + /** + * Returns the task snapshot (thumbnail) for the given `OverviewTaskContainer`. + * If there are no `taskContentView`'s, then the `enableRefactorTaskContentView` feature flag is + * off, in that case fallback to the `snapshotViewRes` id. + */ + private UiObject2 getTaskSnapshot(OverviewTaskContainer overviewTaskContainer) { + UiObject2 taskContentView = mTask.findObject( + mLauncher.getOverviewObjectSelector(overviewTaskContainer.taskContentViewRes)); + if (taskContentView != null) { + BySelector snapshotSelector = mLauncher.getOverviewObjectSelector("snapshot"); + UiObject2 snapshot = mTask.findObject(snapshotSelector); + if (snapshot != null) { + return snapshot; + } + } + + return mTask.findObject( + mLauncher.getOverviewObjectSelector(overviewTaskContainer.snapshotViewRes)); + } + /** * Dismisses the task by swiping up. */ @@ -303,17 +294,13 @@ public final class OverviewTask { } } - 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; } @@ -359,19 +346,25 @@ public final class OverviewTask { */ public enum OverviewTaskContainer { // The main task when the task is not split. - DEFAULT("snapshot", "icon"), + DEFAULT("task_content_view", "snapshot", "icon"), // The first task in split task. - SPLIT_TOP_OR_LEFT("snapshot", "icon"), + SPLIT_TOP_OR_LEFT("task_content_view", "snapshot", "icon"), // The second task in split task. - SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon"), + SPLIT_BOTTOM_OR_RIGHT("bottomright_task_content_view", "bottomright_snapshot", + "bottomRight_icon"), // The desktop task. - DESKTOP("background", "icon"); + DESKTOP("background", "background", "icon"); - public final String snapshotRes; + public final String taskContentViewRes; + // TODO (b/409248525) Delete `snapshotViewRes` when cleaning up + // enableRefactorTaskContentView flag. + public final String snapshotViewRes; public final String iconAppRes; - OverviewTaskContainer(String snapshotRes, String iconAppRes) { - this.snapshotRes = snapshotRes; + OverviewTaskContainer(String taskContentViewRes, String snapshotViewRes, + String iconAppRes) { + this.taskContentViewRes = taskContentViewRes; + this.snapshotViewRes = snapshotViewRes; this.iconAppRes = iconAppRes; } }