diff --git a/quickstep/res/drawable/task_header_close_button.xml b/quickstep/res/drawable/task_header_close_button.xml
new file mode 100644
index 0000000000..b409158c17
--- /dev/null
+++ b/quickstep/res/drawable/task_header_close_button.xml
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/quickstep/res/drawable/task_thumbnail_header_bg.xml b/quickstep/res/drawable/task_thumbnail_header_bg.xml
new file mode 100644
index 0000000000..52ac1ae3cc
--- /dev/null
+++ b/quickstep/res/drawable/task_thumbnail_header_bg.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/quickstep/res/layout/task_thumbnail_view_header.xml b/quickstep/res/layout/task_thumbnail_view_header.xml
new file mode 100644
index 0000000000..ecc1559c10
--- /dev/null
+++ b/quickstep/res/layout/task_thumbnail_view_header.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index b404bb519c..58b3f0ce3c 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -84,6 +84,12 @@
44dp
4dp
+
+ 30dp
+ 18dp
+ 9dp
+ 18dp
+ 16dp
72dp
- 1.1
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index df949c303b..223bc9a08a 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -360,4 +360,12 @@
Circle to Search
+
+
+
+ App icon
+
+ App title
+
+ Close button
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
index d2cb595586..0ee2bd224c 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
@@ -20,7 +20,6 @@ package com.android.quickstep.recents.data
* Container to hold [com.android.launcher3.DeviceProfile] related to Recents.
*
* @property isLargeScreen whether the current device posture has a large screen
+ * @property canEnterDesktopMode whether the current device can enter Desktop UI mode
*/
-data class RecentsDeviceProfile(
- val isLargeScreen: Boolean,
-)
+data class RecentsDeviceProfile(val isLargeScreen: Boolean, val canEnterDesktopMode: Boolean)
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
index c64453d0cd..8450f09cea 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
@@ -17,6 +17,7 @@
package com.android.quickstep.recents.data
import com.android.quickstep.views.RecentsViewContainer
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
/**
* Repository for shrink down version of [com.android.launcher3.DeviceProfile] that only contains
@@ -26,5 +27,10 @@ class RecentsDeviceProfileRepositoryImpl(private val container: RecentsViewConta
RecentsDeviceProfileRepository {
override fun getRecentsDeviceProfile() =
- with(container.deviceProfile) { RecentsDeviceProfile(isLargeScreen = isTablet) }
+ with(container.deviceProfile) {
+ RecentsDeviceProfile(
+ isLargeScreen = isTablet,
+ canEnterDesktopMode = DesktopModeStatus.canEnterDesktopMode(container.asContext()),
+ )
+ }
}
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index dd83af692f..2b364f954d 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -182,6 +182,7 @@ class RecentsDependencies private constructor(private val appContext: Context) {
dispatcherProvider = inject(),
getThumbnailPositionUseCase = inject(),
tasksRepository = inject(),
+ deviceProfileRepository = inject(),
splashAlphaUseCase = inject(scopeId),
)
TaskOverlayViewModel::class.java -> {
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
index 36a86f2526..61185448be 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
@@ -24,18 +24,35 @@ import androidx.annotation.ColorInt
sealed class TaskThumbnailUiState {
data object Uninitialized : TaskThumbnailUiState()
- data object LiveTile : TaskThumbnailUiState()
-
data class BackgroundOnly(@ColorInt val backgroundColor: Int) : TaskThumbnailUiState()
- data class SnapshotSplash(
- val snapshot: Snapshot,
- val splash: Drawable?,
- ) : TaskThumbnailUiState()
+ data class SnapshotSplash(val snapshot: Snapshot, val splash: Drawable?) :
+ TaskThumbnailUiState()
- data class Snapshot(
- val bitmap: Bitmap,
- @Surface.Rotation val thumbnailRotation: Int,
- @ColorInt val backgroundColor: Int
- )
+ 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)
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index b040723a3f..4a7872945c 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -22,11 +22,13 @@ import android.graphics.Outline
import android.graphics.Rect
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.R
import com.android.launcher3.util.ViewPool
import com.android.launcher3.util.coroutines.DispatcherProvider
@@ -39,6 +41,7 @@ import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.views.FixedSizeImageView
+import com.android.quickstep.views.TaskThumbnailViewHeader
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -66,6 +69,8 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
private val splashBackground: View by lazy { findViewById(R.id.splash_background) }
private val splashIcon: FixedSizeImageView by lazy { findViewById(R.id.splash_icon) }
+ private var taskThumbnailViewHeader: TaskThumbnailViewHeader? = null
+
private var uiState: TaskThumbnailUiState = Uninitialized
private val bounds = Rect()
@@ -86,6 +91,12 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
defStyleAttr: Int,
) : super(context, attrs, defStyleAttr)
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+
+ maybeCreateHeader()
+ }
+
override fun onAttachedToWindow() {
super.onAttachedToWindow()
viewAttachedScope =
@@ -102,7 +113,7 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
resetViews()
when (viewModelUiState) {
is Uninitialized -> {}
- is LiveTile -> drawLiveWindow()
+ is LiveTile -> drawLiveWindow(viewModelUiState)
is SnapshotSplash -> drawSnapshotSplash(viewModelUiState)
is BackgroundOnly -> drawBackground(viewModelUiState.backgroundColor)
}
@@ -179,14 +190,20 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
splashIcon.alpha = 0f
scrimView.alpha = 0f
setBackgroundColor(Color.BLACK)
+ taskThumbnailViewHeader?.isInvisible = true
}
private fun drawBackground(@ColorInt background: Int) {
setBackgroundColor(background)
}
- private fun drawLiveWindow() {
+ private fun drawLiveWindow(liveTile: LiveTile) {
liveTileView.isInvisible = false
+
+ if (liveTile is LiveTile.WithHeader) {
+ taskThumbnailViewHeader?.isInvisible = false
+ taskThumbnailViewHeader?.setHeader(liveTile.header)
+ }
}
private fun drawSnapshotSplash(snapshotSplash: SnapshotSplash) {
@@ -197,6 +214,11 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
}
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
@@ -210,4 +232,14 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
private companion object {
const val TAG = "TaskThumbnailView"
}
+
+ private fun maybeCreateHeader() {
+ if (enableDesktopExplodedView() && taskThumbnailViewHeader == null) {
+ taskThumbnailViewHeader =
+ LayoutInflater.from(context)
+ .inflate(R.layout.task_thumbnail_view_header, this, false)
+ as TaskThumbnailViewHeader
+ addView(taskThumbnailViewHeader)
+ }
+ }
}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
index b5b2fc9407..a154c3cba7 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -18,11 +18,14 @@ package com.android.quickstep.task.viewmodel
import android.annotation.ColorInt
import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.graphics.Matrix
import android.util.Log
import androidx.core.graphics.ColorUtils
+import com.android.launcher3.Flags.enableDesktopExplodedView
import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState
import com.android.quickstep.recents.viewmodel.RecentsViewData
@@ -32,6 +35,7 @@ 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
import com.android.systemui.shared.recents.model.Task
import kotlin.math.max
@@ -51,6 +55,7 @@ class TaskThumbnailViewModelImpl(
taskContainerData: TaskContainerData,
dispatcherProvider: DispatcherProvider,
private val tasksRepository: RecentTasksRepository,
+ private val deviceProfileRepository: RecentsDeviceProfileRepository,
private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
private val splashAlphaUseCase: SplashAlphaUseCase,
) : TaskThumbnailViewModel {
@@ -90,7 +95,7 @@ class TaskThumbnailViewModelImpl(
// )
when {
taskVal == null -> Uninitialized
- isRunning -> LiveTile
+ isRunning -> createLiveTileState(taskVal)
isBackgroundOnly(taskVal) ->
BackgroundOnly(taskVal.colorBackground.removeAlpha())
isSnapshotSplashState(taskVal) ->
@@ -129,7 +134,46 @@ class TaskThumbnailViewModelImpl(
private fun createSnapshotState(task: Task): Snapshot {
val thumbnailData = task.thumbnail
val bitmap = thumbnailData?.thumbnail!!
- return Snapshot(bitmap, thumbnailData.rotation, task.colorBackground.removeAlpha())
+ var thumbnailHeader = maybeCreateHeader(task)
+ return if (thumbnailHeader != null)
+ Snapshot.WithHeader(
+ bitmap,
+ thumbnailData.rotation,
+ task.colorBackground.removeAlpha(),
+ thumbnailHeader,
+ )
+ else
+ Snapshot.WithoutHeader(
+ bitmap,
+ thumbnailData.rotation,
+ task.colorBackground.removeAlpha(),
+ )
+ }
+
+ private fun shouldHaveThumbnailHeader(task: Task): Boolean {
+ return deviceProfileRepository.getRecentsDeviceProfile().canEnterDesktopMode &&
+ enableDesktopExplodedView() &&
+ task.key.windowingMode == WINDOWING_MODE_FREEFORM
+ }
+
+ private fun maybeCreateHeader(task: Task): ThumbnailHeader? {
+ // Header is only needed when this task is a desktop task and Overivew exploded view is
+ // enabled.
+ if (!shouldHaveThumbnailHeader(task)) {
+ return null
+ }
+
+ // TODO(http://b/353965691): figure out what to do when `icon` or `titleDescription` is
+ // null.
+ val icon = task.icon ?: return null
+ val titleDescription = task.titleDescription ?: return null
+ return ThumbnailHeader(icon, titleDescription)
+ }
+
+ private fun createLiveTileState(task: Task): LiveTile {
+ val thumbnailHeader = maybeCreateHeader(task)
+ return if (thumbnailHeader != null) LiveTile.WithHeader(thumbnailHeader)
+ else LiveTile.WithoutHeader
}
@ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt
new file mode 100644
index 0000000000..9eb294ac76
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.launcher3.R
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
+
+class TaskThumbnailViewHeader
+@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) }
+
+ fun setHeader(header: ThumbnailHeader) {
+ headerTitleView.setText(header.title)
+ headerIconView.setImageDrawable(header.icon)
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
index fc2f029419..4e909035bb 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
@@ -18,9 +18,7 @@ package com.android.quickstep.recents.data
class FakeRecentsDeviceProfileRepository : RecentsDeviceProfileRepository {
private var recentsDeviceProfile =
- RecentsDeviceProfile(
- isLargeScreen = false,
- )
+ RecentsDeviceProfile(isLargeScreen = false, canEnterDesktopMode = false)
override fun getRecentsDeviceProfile() = recentsDeviceProfile
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
deleted file mode 100644
index abe414238d..0000000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.recents.data
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.launcher3.FakeInvariantDeviceProfileTest
-import com.android.quickstep.views.RecentsViewContainer
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-/** Test for [RecentsDeviceProfileRepositoryImpl] */
-@RunWith(AndroidJUnit4::class)
-class RecentsDeviceProfileRepositoryImplTest : FakeInvariantDeviceProfileTest() {
- private val recentsViewContainer = mock()
-
- private val systemUnderTest = RecentsDeviceProfileRepositoryImpl(recentsViewContainer)
-
- @Test
- fun deviceProfileMappedCorrectly() {
- initializeVarsForTablet()
- val tabletDeviceProfile = newDP()
- whenever(recentsViewContainer.deviceProfile).thenReturn(tabletDeviceProfile)
-
- assertThat(systemUnderTest.getRecentsDeviceProfile())
- .isEqualTo(RecentsDeviceProfile(isLargeScreen = true))
- }
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
index a777bd47aa..a956c9c969 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
@@ -16,16 +16,23 @@
package com.android.quickstep.task.thumbnail
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.content.ComponentName
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.drawable.Drawable
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import android.view.Surface
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Flags
import com.android.launcher3.util.TestDispatcherProvider
+import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.data.RecentsDeviceProfile
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
@@ -34,6 +41,7 @@ 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
import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
@@ -44,6 +52,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
@@ -52,6 +61,8 @@ import org.mockito.kotlin.whenever
/** Test for [TaskThumbnailView] */
@RunWith(AndroidJUnit4::class)
class TaskThumbnailViewModelImplTest {
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
private val dispatcher = StandardTestDispatcher()
private val testScope = TestScope(dispatcher)
@@ -59,6 +70,7 @@ class TaskThumbnailViewModelImplTest {
private val taskContainerData = TaskContainerData()
private val dispatcherProvider = TestDispatcherProvider(dispatcher)
private val tasksRepository = FakeTasksRepository()
+ private val deviceProfileRepository = FakeRecentsDeviceProfileRepository()
private val mGetThumbnailPositionUseCase = mock()
private val splashAlphaUseCase: SplashAlphaUseCase = mock()
@@ -68,12 +80,18 @@ class TaskThumbnailViewModelImplTest {
taskContainerData,
dispatcherProvider,
tasksRepository,
+ deviceProfileRepository,
mGetThumbnailPositionUseCase,
splashAlphaUseCase,
)
}
- private val tasks = (0..5).map(::createTaskWithId)
+ private val fullscreenTaskIdRange: IntRange = 0..5
+ private val freeformTaskIdRange: IntRange = 6..10
+
+ private val fullscreenTasks = fullscreenTaskIdRange.map(::createTaskWithId)
+ private val freeformTasks = freeformTaskIdRange.map(::createFreeformTaskWithId)
+ private val tasks = fullscreenTasks + freeformTasks
@Test
fun initialStateIsUninitialized() =
@@ -88,7 +106,7 @@ class TaskThumbnailViewModelImplTest {
recentsViewData.runningTaskIds.value = setOf(taskId)
systemUnderTest.bind(taskId)
- assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
+ assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile.WithoutHeader)
}
@Test
@@ -108,7 +126,7 @@ class TaskThumbnailViewModelImplTest {
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
SnapshotSplash(
- Snapshot(
+ Snapshot.WithoutHeader(
backgroundColor = Color.rgb(1, 1, 1),
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_0,
@@ -127,7 +145,7 @@ class TaskThumbnailViewModelImplTest {
tasksRepository.setVisibleTasks(setOf(runningTaskId, stoppedTaskId))
recentsViewData.runningTaskIds.value = setOf(runningTaskId)
systemUnderTest.bind(runningTaskId)
- assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
+ assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile.WithoutHeader)
systemUnderTest.bind(stoppedTaskId)
assertThat(systemUnderTest.uiState.first())
@@ -175,7 +193,7 @@ class TaskThumbnailViewModelImplTest {
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
SnapshotSplash(
- Snapshot(
+ Snapshot.WithoutHeader(
backgroundColor = Color.rgb(2, 2, 2),
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_270,
@@ -202,7 +220,7 @@ class TaskThumbnailViewModelImplTest {
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
SnapshotSplash(
- Snapshot(
+ Snapshot.WithoutHeader(
backgroundColor = Color.rgb(2, 2, 2),
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_0,
@@ -212,6 +230,57 @@ class TaskThumbnailViewModelImplTest {
)
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
+ fun bindRunningTask_inDesktop_thenStateIs_LiveTile_withHeader() =
+ testScope.runTest {
+ deviceProfileRepository.setRecentsDeviceProfile(
+ RecentsDeviceProfile(isLargeScreen = true, canEnterDesktopMode = true)
+ )
+
+ val taskId = freeformTaskIdRange.first
+ val expectedIconData = mock()
+ tasksRepository.seedIconData(taskId, "Task $taskId", "Task $taskId", expectedIconData)
+ tasksRepository.seedTasks(freeformTasks)
+ tasksRepository.setVisibleTasks(setOf(taskId))
+ recentsViewData.runningTaskIds.value = setOf(taskId)
+ systemUnderTest.bind(taskId)
+
+ assertThat(systemUnderTest.uiState.first())
+ .isEqualTo(LiveTile.WithHeader(ThumbnailHeader(expectedIconData, "Task $taskId")))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
+ fun bindStoppedTaskWithThumbnail_inDesktop_thenStateIs_SnapshotSplash_withHeader() =
+ testScope.runTest {
+ deviceProfileRepository.setRecentsDeviceProfile(
+ RecentsDeviceProfile(isLargeScreen = true, canEnterDesktopMode = true)
+ )
+
+ val taskId = freeformTaskIdRange.first
+ val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_0)
+ tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
+ val expectedIconData = mock()
+ tasksRepository.seedIconData(taskId, "Task $taskId", "Task $taskId", expectedIconData)
+ tasksRepository.seedTasks(freeformTasks)
+ tasksRepository.setVisibleTasks(setOf(taskId))
+
+ systemUnderTest.bind(taskId)
+ assertThat(systemUnderTest.uiState.first())
+ .isEqualTo(
+ SnapshotSplash(
+ Snapshot.WithHeader(
+ backgroundColor = Color.rgb(taskId, taskId, taskId),
+ bitmap = expectedThumbnailData.thumbnail!!,
+ thumbnailRotation = Surface.ROTATION_0,
+ header = ThumbnailHeader(expectedIconData, "Task $taskId"),
+ ),
+ expectedIconData,
+ )
+ )
+ }
+
@Test
fun getSnapshotMatrix_MissingThumbnail() =
testScope.runTest {
@@ -269,9 +338,38 @@ class TaskThumbnailViewModelImplTest {
}
private fun createTaskWithId(taskId: Int) =
- Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
- colorBackground = Color.argb(taskId, taskId, taskId, taskId)
- }
+ Task(
+ Task.TaskKey(
+ taskId,
+ WINDOWING_MODE_FULLSCREEN,
+ Intent(),
+ ComponentName("", ""),
+ 0,
+ 2000,
+ )
+ )
+ .apply {
+ colorBackground = Color.argb(taskId, taskId, taskId, taskId)
+ titleDescription = "Task $taskId"
+ icon = mock()
+ }
+
+ private fun createFreeformTaskWithId(taskId: Int) =
+ Task(
+ Task.TaskKey(
+ taskId,
+ WINDOWING_MODE_FREEFORM,
+ Intent(),
+ ComponentName("", ""),
+ 0,
+ 2000,
+ )
+ )
+ .apply {
+ colorBackground = Color.argb(taskId, taskId, taskId, taskId)
+ titleDescription = "Task $taskId"
+ icon = mock()
+ }
private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
val bitmap = mock()
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt b/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt
new file mode 100644
index 0000000000..418d66c616
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.launcher3.FakeInvariantDeviceProfileTest
+import com.android.quickstep.recents.data.RecentsDeviceProfile
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepositoryImpl
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+class RecentsDeviceProfileRepositoryImplTest : FakeInvariantDeviceProfileTest() {
+ private val recentsViewContainer: RecentsViewContainer = mock()
+
+ private lateinit var mockitoSession: StaticMockitoSession
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ mockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .mockStatic(DesktopModeStatus::class.java)
+ .startMocking()
+ whenever(recentsViewContainer.asContext()).thenReturn(context)
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ fun deviceProfileMappedCorrectlyForPhone() {
+ val deviceProfileRepo = RecentsDeviceProfileRepositoryImpl(recentsViewContainer)
+ initializeVarsForPhone()
+ val phoneDeviceProfile = newDP()
+ whenever(recentsViewContainer.deviceProfile).thenReturn(phoneDeviceProfile)
+
+ whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false)
+ assertThat(deviceProfileRepo.getRecentsDeviceProfile())
+ .isEqualTo(RecentsDeviceProfile(isLargeScreen = false, canEnterDesktopMode = false))
+ }
+
+ @Test
+ fun deviceProfileMappedCorrectlyForTablet() {
+ val deviceProfileRepo = RecentsDeviceProfileRepositoryImpl(recentsViewContainer)
+ initializeVarsForTablet()
+ val tabletDeviceProfile = newDP()
+ whenever(recentsViewContainer.deviceProfile).thenReturn(tabletDeviceProfile)
+
+ whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
+ assertThat(deviceProfileRepo.getRecentsDeviceProfile())
+ .isEqualTo(RecentsDeviceProfile(isLargeScreen = true, canEnterDesktopMode = true))
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index 954dc8fb59..bfbdb18696 100644
--- a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -58,7 +58,7 @@ abstract class FakeInvariantDeviceProfileTest {
@Rule @JvmField val limitDevicesRule = LimitDevicesRule()
@Before
- fun setUp() {
+ open fun setUp() {
context = ApplicationProvider.getApplicationContext()
// make sure to reset values
useTwoPanels = false
@@ -83,7 +83,7 @@ abstract class FakeInvariantDeviceProfileTest {
protected fun initializeVarsForPhone(
isGestureMode: Boolean = true,
- isVerticalBar: Boolean = false
+ isVerticalBar: Boolean = false,
) {
val (x, y) = if (isVerticalBar) Pair(2400, 1080) else Pair(1080, 2400)
@@ -94,8 +94,8 @@ abstract class FakeInvariantDeviceProfileTest {
if (isVerticalBar) 118 else 0,
if (isVerticalBar) 74 else 118,
if (!isGestureMode && isVerticalBar) 126 else 0,
- if (isGestureMode) 63 else if (isVerticalBar) 0 else 126
- )
+ if (isGestureMode) 63 else if (isVerticalBar) 0 else 126,
+ ),
)
whenever(info.isTablet(any())).thenReturn(false)
@@ -121,7 +121,7 @@ abstract class FakeInvariantDeviceProfileTest {
PointF(80f, 104f),
PointF(80f, 104f),
PointF(80f, 104f),
- PointF(80f, 104f)
+ PointF(80f, 104f),
)
.toTypedArray()
@@ -143,7 +143,7 @@ abstract class FakeInvariantDeviceProfileTest {
PointF(80f, 104f),
PointF(80f, 104f),
PointF(80f, 104f),
- PointF(80f, 104f)
+ PointF(80f, 104f),
)
.toTypedArray()
allAppsIconSize = floatArrayOf(60f, 60f, 60f, 60f)
@@ -174,7 +174,7 @@ abstract class FakeInvariantDeviceProfileTest {
protected fun initializeVarsForTablet(
isLandscape: Boolean = false,
- isGestureMode: Boolean = true
+ isGestureMode: Boolean = true,
) {
val (x, y) = if (isLandscape) Pair(2560, 1600) else Pair(1600, 2560)
@@ -203,7 +203,7 @@ abstract class FakeInvariantDeviceProfileTest {
PointF(102f, 120f),
PointF(120f, 104f),
PointF(102f, 120f),
- PointF(102f, 120f)
+ PointF(102f, 120f),
)
.toTypedArray()
@@ -225,7 +225,7 @@ abstract class FakeInvariantDeviceProfileTest {
PointF(96f, 142f),
PointF(126f, 126f),
PointF(96f, 142f),
- PointF(96f, 142f)
+ PointF(96f, 142f),
)
.toTypedArray()
allAppsIconSize = FloatArray(4) { 60f }
@@ -288,7 +288,7 @@ abstract class FakeInvariantDeviceProfileTest {
PointF(80f, 104f),
PointF(80f, 104f),
PointF(68f, 116f),
- PointF(80f, 102f)
+ PointF(80f, 102f),
)
.toTypedArray()