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()