Merge "Refactor: Extract splash alpha logic from TaskThumbnailViewModel" into main
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 1f428f3..d2f10b6 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -28,13 +28,11 @@
import com.android.quickstep.recents.data.TasksRepository
import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.GetThumbnailUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
-import com.android.quickstep.task.thumbnail.TaskThumbnailViewData
-import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
@@ -175,14 +173,8 @@
val instance: Any =
when (modelClass) {
RecentsViewData::class.java -> RecentsViewData()
- TaskContainerData::class.java -> TaskContainerData()
- TaskThumbnailViewData::class.java -> TaskThumbnailViewData()
TaskThumbnailViewModel::class.java ->
- TaskThumbnailViewModelImpl(
- dispatcherProvider = inject(),
- getThumbnailPositionUseCase = inject(),
- splashAlphaUseCase = inject(scopeId),
- )
+ TaskThumbnailViewModelImpl(getThumbnailPositionUseCase = inject())
TaskOverlayViewModel::class.java -> {
val task = extras["Task"] as Task
TaskOverlayViewModel(
@@ -193,6 +185,8 @@
dispatcherProvider = inject(),
)
}
+ IsThumbnailValidUseCase::class.java ->
+ IsThumbnailValidUseCase(rotationStateRepository = inject())
GetTaskUseCase::class.java -> GetTaskUseCase(repository = inject())
GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
GetSysUiStatusNavFlagsUseCase::class.java -> GetSysUiStatusNavFlagsUseCase()
@@ -203,14 +197,6 @@
tasksRepository = inject(),
)
OrganizeDesktopTasksUseCase::class.java -> OrganizeDesktopTasksUseCase()
- SplashAlphaUseCase::class.java ->
- SplashAlphaUseCase(
- recentsViewData = inject(),
- taskContainerData = inject(scopeId),
- taskThumbnailViewData = inject(scopeId),
- tasksRepository = inject(),
- rotationStateRepository = inject(),
- )
else -> {
log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR)
error("Factory for ${modelClass.simpleName} not defined!")
diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt
new file mode 100644
index 0000000..02f8329
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.recents.domain.usecase
+
+import android.graphics.Bitmap
+import android.view.Surface
+import com.android.quickstep.recents.data.RecentsRotationStateRepository
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.android.systemui.shared.recents.utilities.Utilities
+
+/**
+ * Use case responsible for validating the aspect ratio and rotation of a thumbnail against the
+ * expected values based on the view's dimensions and the current rotation state.
+ *
+ * This class checks if the thumbnail's aspect ratio significantly differs from the aspect ratio of
+ * the view it is intended to be displayed in, and if the thumbnail's rotation is consistent with
+ * the device's current rotation state.
+ *
+ * @property rotationStateRepository Repository providing the current rotation state of the device.
+ */
+class IsThumbnailValidUseCase(private val rotationStateRepository: RecentsRotationStateRepository) {
+ operator fun invoke(thumbnailData: ThumbnailData?, viewWidth: Int, viewHeight: Int): Boolean {
+ val thumbnail = thumbnailData?.thumbnail ?: return false
+ return !isInaccurateThumbnail(thumbnail, viewWidth, viewHeight, thumbnailData.rotation)
+ }
+
+ private fun isInaccurateThumbnail(
+ thumbnail: Bitmap,
+ viewWidth: Int,
+ viewHeight: Int,
+ rotation: Int,
+ ): Boolean =
+ isAspectRatioDifferentFromViewAspectRatio(
+ thumbnail = thumbnail,
+ width = viewWidth.toFloat(),
+ height = viewHeight.toFloat(),
+ ) || isRotationDifferentFromTask(rotation)
+
+ private fun isAspectRatioDifferentFromViewAspectRatio(
+ thumbnail: Bitmap,
+ width: Float,
+ height: Float,
+ ): Boolean {
+ return Utilities.isRelativePercentDifferenceGreaterThan(
+ /* first = */ width / height,
+ /* second = */ thumbnail.width / thumbnail.height.toFloat(),
+ /* bound = */ PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT,
+ )
+ }
+
+ private fun isRotationDifferentFromTask(thumbnailRotation: Int): Boolean {
+ val rotationState = rotationStateRepository.getRecentsRotationState()
+ return if (rotationState.orientationHandlerRotation == Surface.ROTATION_0) {
+ (rotationState.activityRotation - thumbnailRotation) % 2 != 0
+ } else {
+ rotationState.orientationHandlerRotation != thumbnailRotation
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
index 961446f..162d14d 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
@@ -24,8 +24,10 @@
import com.android.quickstep.recents.domain.model.TaskModel
import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.ThumbnailData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -45,6 +47,7 @@
recentsViewData: RecentsViewData,
private val getTaskUseCase: GetTaskUseCase,
private val getSysUiStatusNavFlagsUseCase: GetSysUiStatusNavFlagsUseCase,
+ private val isThumbnailValidUseCase: IsThumbnailValidUseCase,
dispatcherProvider: DispatcherProvider,
) {
private var taskIds = MutableStateFlow(emptySet<Int>())
@@ -78,6 +81,9 @@
taskIds.value = taskId.toSet()
}
+ fun isThumbnailValid(thumbnail: ThumbnailData?, width: Int, height: Int): Boolean =
+ isThumbnailValidUseCase(thumbnail, width, height)
+
private fun mapToTaskTile(tasks: List<TaskData>, isLiveTile: Boolean): TaskTileUiState {
val firstThumbnailData = (tasks.firstOrNull() as? TaskData.Data)?.thumbnailData
return TaskTileUiState(
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
index a1f8454..2465a46 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
@@ -27,8 +27,6 @@
// The settled set of visible taskIds that is updated after RecentsView scroll settles.
val settledFullyVisibleTaskIds = MutableStateFlow(emptySet<Int>())
- val thumbnailSplashProgress = MutableStateFlow(0f)
-
// A list of taskIds that are associated with a RecentsAnimationController. */
val runningTaskIds = MutableStateFlow(emptySet<Int>())
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index 73332fc..5ff8aaa 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -42,10 +42,6 @@
recentsViewData.overlayEnabled.value = isOverlayEnabled
}
- fun updateThumbnailSplashProgress(taskThumbnailSplashAlpha: Float) {
- recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha
- }
-
suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map<Int, ThumbnailData>?) {
if (updatedThumbnails.isNullOrEmpty()) return
combine(
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
deleted file mode 100644
index 723df55..0000000
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
+++ /dev/null
@@ -1,26 +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.viewmodel
-
-import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.runBlocking
-
-class TaskContainerViewModel(private val splashAlphaUseCase: SplashAlphaUseCase) {
- fun shouldShowThumbnailSplash(taskId: Int): Boolean =
- (runBlocking { splashAlphaUseCase.execute(taskId).firstOrNull() } ?: 0f) > 0f
-}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt b/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt
deleted file mode 100644
index 7673c71..0000000
--- a/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt
+++ /dev/null
@@ -1,90 +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.task.thumbnail
-
-import android.graphics.Bitmap
-import android.view.Surface
-import com.android.quickstep.recents.data.RecentTasksRepository
-import com.android.quickstep.recents.data.RecentsRotationStateRepository
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.viewmodel.TaskContainerData
-import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
-import com.android.systemui.shared.recents.utilities.Utilities
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-
-class SplashAlphaUseCase(
- private val recentsViewData: RecentsViewData,
- private val taskContainerData: TaskContainerData,
- private val taskThumbnailViewData: TaskThumbnailViewData,
- private val tasksRepository: RecentTasksRepository,
- private val rotationStateRepository: RecentsRotationStateRepository,
-) {
- fun execute(taskId: Int): Flow<Float> =
- combine(
- taskThumbnailViewData.width,
- taskThumbnailViewData.height,
- tasksRepository.getThumbnailById(taskId),
- taskContainerData.thumbnailSplashProgress,
- recentsViewData.thumbnailSplashProgress
- ) { width, height, thumbnailData, taskSplashProgress, globalSplashProgress ->
- val thumbnail = thumbnailData?.thumbnail
- when {
- thumbnail == null -> 0f
- taskSplashProgress > 0f -> taskSplashProgress
- globalSplashProgress > 0f &&
- isInaccurateThumbnail(thumbnail, thumbnailData.rotation, width, height) ->
- globalSplashProgress
- else -> 0f
- }
- }
- .distinctUntilChanged()
-
- private fun isInaccurateThumbnail(
- thumbnail: Bitmap,
- thumbnailRotation: Int,
- width: Int,
- height: Int
- ): Boolean {
- return isThumbnailAspectRatioDifferentFromThumbnailData(thumbnail, width, height) ||
- isThumbnailRotationDifferentFromTask(thumbnailRotation)
- }
-
- private fun isThumbnailAspectRatioDifferentFromThumbnailData(
- thumbnail: Bitmap,
- viewWidth: Int,
- viewHeight: Int
- ): Boolean {
- val viewAspect: Float = viewWidth / viewHeight.toFloat()
- val thumbnailAspect: Float = thumbnail.width / thumbnail.height.toFloat()
- return Utilities.isRelativePercentDifferenceGreaterThan(
- viewAspect,
- thumbnailAspect,
- PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT
- )
- }
-
- private fun isThumbnailRotationDifferentFromTask(thumbnailRotation: Int): Boolean {
- val rotationState = rotationStateRepository.getRecentsRotationState()
- return if (rotationState.orientationHandlerRotation == Surface.ROTATION_0) {
- (rotationState.activityRotation - thumbnailRotation) % 2 != 0
- } else {
- rotationState.orientationHandlerRotation != thumbnailRotation
- }
- }
-}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 28152ec..63e93ba 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -50,10 +50,6 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.dropWhile
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
@@ -62,8 +58,6 @@
// This is initialised here and set in onAttachedToWindow because onLayout can be called before
// onAttachedToWindow so this property needs to be initialised as it is used below.
- private var viewData: TaskThumbnailViewData = RecentsDependencies.get(this)
-
private lateinit var viewModel: TaskThumbnailViewModel
private lateinit var viewAttachedScope: CoroutineScope
@@ -110,18 +104,7 @@
CoroutineScope(
SupervisorJob() + Dispatchers.Main.immediate + CoroutineName("TaskThumbnailView")
)
- viewData = RecentsDependencies.get(this)
- updateViewDataValues()
viewModel = RecentsDependencies.get(this)
- viewModel.splashAlpha
- .dropWhile { it == 0f }
- .flowOn(dispatcherProvider.background)
- .onEach { splashAlpha ->
- splashBackground.alpha = splashAlpha
- splashIcon.alpha = splashAlpha
- }
- .launchIn(viewAttachedScope)
-
clipToOutline = true
outlineProvider =
object : ViewOutlineProvider() {
@@ -144,16 +127,9 @@
resetViews()
}
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- super.onLayout(changed, left, top, right, bottom)
- if (changed) {
- updateViewDataValues()
- }
- }
-
fun setState(state: TaskThumbnailUiState, taskId: Int? = null) {
- logDebug("taskId: $taskId - uiState changed from: $uiState to: $state")
if (uiState == state) return
+ logDebug("taskId: $taskId - uiState changed from: $uiState to: $state")
uiState = state
resetViews()
when (state) {
@@ -164,6 +140,12 @@
}
}
+ /**
+ * Updates the alpha of the dim layer on top of this view. If dimAlpha is 0, no dimming is
+ * applied; if dimAlpha is 1, the thumbnail will be the extracted background color.
+ *
+ * @param tintAmount The amount of alpha that will be applied to the dim layer.
+ */
fun updateTintAmount(tintAmount: Float) {
dimAlpha[ScrimViewAlpha.TintAmount.ordinal].value = tintAmount
}
@@ -172,9 +154,9 @@
dimAlpha[ScrimViewAlpha.MenuProgress.ordinal].value = progress * MAX_SCRIM_ALPHA
}
- private fun updateViewDataValues() {
- viewData.width.value = width
- viewData.height.value = height
+ fun updateSplashAlpha(value: Float) {
+ splashBackground.alpha = value
+ splashIcon.alpha = value
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
deleted file mode 100644
index 3502029..0000000
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
+++ /dev/null
@@ -1,24 +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.task.thumbnail
-
-import kotlinx.coroutines.flow.MutableStateFlow
-
-class TaskThumbnailViewData {
- val width = MutableStateFlow(0)
- val height = MutableStateFlow(0)
-}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
deleted file mode 100644
index 279ce39..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
+++ /dev/null
@@ -1,23 +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.task.viewmodel
-
-import kotlinx.coroutines.flow.MutableStateFlow
-
-class TaskContainerData {
- val thumbnailSplashProgress = MutableStateFlow(0f)
-}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
index c89bf01..e641737 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -17,13 +17,9 @@
package com.android.quickstep.task.viewmodel
import android.graphics.Matrix
-import kotlinx.coroutines.flow.Flow
/** ViewModel for representing TaskThumbnails */
interface TaskThumbnailViewModel {
- /** Provides the alpha of the splash icon */
- val splashAlpha: Flow<Float>
-
/** Attaches this ViewModel to a specific task id for it to provide data from. */
fun bind(taskId: Int)
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
index 635d08b..94c40d1 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -19,32 +19,17 @@
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.graphics.Matrix
import android.util.Log
-import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState
-import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.flowOn
-@OptIn(ExperimentalCoroutinesApi::class)
class TaskThumbnailViewModelImpl(
- dispatcherProvider: DispatcherProvider,
- private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
- private val splashAlphaUseCase: SplashAlphaUseCase,
+ private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase
) : TaskThumbnailViewModel {
- private val splashProgress = MutableStateFlow(flowOf(0f))
private var taskId: Int = INVALID_TASK_ID
- override val splashAlpha =
- splashProgress.flatMapLatest { it }.flowOn(dispatcherProvider.background)
-
override fun bind(taskId: Int) {
Log.d(TAG, "bind taskId: $taskId")
this.taskId = taskId
- splashProgress.value = splashAlphaUseCase.execute(taskId)
}
override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix =
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 0182969..99255e8 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -52,9 +52,9 @@
import com.android.launcher3.QuickstepTransitionManager
import com.android.launcher3.R
import com.android.launcher3.Utilities
+import com.android.launcher3.anim.AnimatedFloat
import com.android.launcher3.anim.PendingAnimation
import com.android.launcher3.apppairs.AppPairIcon
-import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.logging.StatsLogManager.EventEnum
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.statehandlers.DepthController
@@ -94,7 +94,7 @@
val fadeWithThumbnail: Boolean,
val isStagedTask: Boolean,
val iconView: View?,
- val contentDescription: CharSequence?
+ val contentDescription: CharSequence?,
)
}
@@ -104,7 +104,7 @@
*/
fun getFirstAnimInitViews(
taskViewSupplier: Supplier<TaskView>,
- splitSelectSourceSupplier: Supplier<SplitSelectSource?>
+ splitSelectSourceSupplier: Supplier<SplitSelectSource?>,
): SplitAnimInitProps {
val splitSelectSource = splitSelectSourceSupplier.get()
if (!splitSelectStateController.isAnimateCurrentTaskDismissal) {
@@ -116,7 +116,7 @@
fadeWithThumbnail = false,
isStagedTask = true,
iconView = null,
- splitSelectSource.itemInfo.contentDescription
+ splitSelectSource.itemInfo.contentDescription,
)
} else if (splitSelectStateController.isDismissingFromSplitPair) {
// Initiating split from overview, but on a split pair
@@ -131,7 +131,7 @@
fadeWithThumbnail = true,
isStagedTask = true,
iconView = container.iconView.asView(),
- container.task.titleDescription
+ container.task.titleDescription,
)
}
}
@@ -151,7 +151,7 @@
fadeWithThumbnail = true,
isStagedTask = true,
iconView = it.iconView.asView(),
- it.task.titleDescription
+ it.task.titleDescription,
)
}
}
@@ -189,29 +189,25 @@
deviceProfile: DeviceProfile,
taskViewWidth: Int,
taskViewHeight: Int,
- isPrimaryTaskSplitting: Boolean
+ isPrimaryTaskSplitting: Boolean,
) {
val snapshot = taskContainer.snapshotView
val iconView: View = taskContainer.iconView.asView()
- if (!enableRefactorTaskThumbnail()) {
+ if (enableRefactorTaskThumbnail()) {
+ builder.add(
+ AnimatedFloat { v -> taskContainer.taskView.splitSplashAlpha = v }
+ .animateToValue(1f)
+ )
+ } else {
val thumbnailViewDeprecated = taskContainer.thumbnailViewDeprecated
builder.add(
ObjectAnimator.ofFloat(
thumbnailViewDeprecated,
TaskThumbnailViewDeprecated.SPLASH_ALPHA,
- 1f
+ 1f,
)
)
thumbnailViewDeprecated.setShowSplashForSplitSelection(true)
- } else {
- builder.add(
- ValueAnimator.ofFloat(0f, 1f).apply {
- addUpdateListener {
- taskContainer.taskContainerData.thumbnailSplashProgress.value =
- it.animatedFraction
- }
- }
- )
}
// With the new `IconAppChipView`, we always want to keep the chip pinned to the
// top left of the task / thumbnail.
@@ -220,7 +216,7 @@
ObjectAnimator.ofFloat(
(iconView as IconAppChipView).splitTranslationX,
MULTI_PROPERTY_VALUE,
- 0f
+ 0f,
)
)
builder.add(
@@ -306,7 +302,7 @@
fun addScrimBehindAnim(
pendingAnimation: PendingAnimation,
container: RecentsViewContainer,
- context: Context
+ context: Context,
): View {
val scrim = View(context)
val recentsView = container.getOverviewPanel<RecentsView<*, *>>()
@@ -334,8 +330,8 @@
Interpolators.clampToProgress(
timings.backingScrimFadeInterpolator,
timings.backingScrimFadeInStartOffset,
- timings.backingScrimFadeInEndOffset
- )
+ timings.backingScrimFadeInEndOffset,
+ ),
)
return scrim
@@ -358,7 +354,7 @@
fun createPlaceholderDismissAnim(
container: RecentsViewContainer,
splitDismissEvent: EventEnum,
- duration: Long?
+ duration: Long?,
): AnimatorSet {
val animatorSet = AnimatorSet()
duration?.let { animatorSet.duration = it }
@@ -375,7 +371,7 @@
Rect(0, 0, floatingTask.width, floatingTask.height),
false,
null,
- onScreenRectF
+ onScreenRectF,
)
// Get the part of the floatingTask that intersects with the DragLayer (i.e. the
// on-screen portion)
@@ -383,7 +379,7 @@
dragLayer.left.toFloat(),
dragLayer.top.toFloat(),
dragLayer.right.toFloat(),
- dragLayer.bottom.toFloat()
+ dragLayer.bottom.toFloat(),
)
animatorSet.play(
ObjectAnimator.ofFloat(
@@ -393,8 +389,8 @@
floatingTask,
onScreenRectF,
floatingTask.stagePosition,
- container.deviceProfile
- )
+ container.deviceProfile,
+ ),
)
)
animatorSet.addListener(
@@ -403,7 +399,7 @@
splitSelectStateController.resetState()
safeRemoveViewFromDragLayer(
container,
- splitSelectStateController.splitInstructionsView
+ splitSelectStateController.splitInstructionsView,
)
}
}
@@ -429,8 +425,8 @@
Interpolators.clampToProgress(
Interpolators.LINEAR,
timings.instructionsContainerFadeInStartOffset,
- timings.instructionsContainerFadeInEndOffset
- )
+ timings.instructionsContainerFadeInEndOffset,
+ ),
)
anim.addFloat(
splitInstructionsView,
@@ -440,8 +436,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED_DECELERATE,
timings.instructionsUnfoldStartOffset,
- timings.instructionsUnfoldEndOffset
- )
+ timings.instructionsUnfoldEndOffset,
+ ),
)
return anim
}
@@ -459,7 +455,7 @@
fun playAnimPlaceholderToFullscreen(
container: RecentsViewContainer,
view: View,
- resetCallback: Optional<Runnable>
+ resetCallback: Optional<Runnable>,
) {
val stagedTaskView = view as FloatingTaskView
@@ -481,7 +477,7 @@
RectF(firstTaskStartingBounds),
firstTaskEndingBounds,
false /* fadeWithThumbnail */,
- true /* isStagedTask */
+ true, /* isStagedTask */
)
pendingAnimation.addEndListener {
@@ -511,7 +507,7 @@
info: TransitionInfo?,
t: Transaction?,
finishCallback: Runnable,
- cornerRadius: Float
+ cornerRadius: Float,
) {
if (info == null && t == null) {
// (Legacy animation) Tapping a split tile in Overview
@@ -530,7 +526,7 @@
nonApps,
stateManager,
depthController,
- finishCallback
+ finishCallback,
)
return
@@ -548,7 +544,7 @@
depthController,
info,
t,
- finishCallback
+ finishCallback,
)
} else if (launchingIconView != null) {
// Tapping an app pair icon
@@ -563,7 +559,7 @@
info,
t,
finishCallback,
- cornerRadius
+ cornerRadius,
)
} else {
composeFullscreenIconSplitLaunchAnimator(
@@ -571,7 +567,7 @@
info,
t,
finishCallback,
- appPairLaunchingAppIndex
+ appPairLaunchingAppIndex,
)
}
} else {
@@ -587,7 +583,7 @@
info,
t,
finishCallback,
- cornerRadius
+ cornerRadius,
)
}
}
@@ -603,7 +599,7 @@
depthController: DepthController?,
info: TransitionInfo,
t: Transaction,
- finishCallback: Runnable
+ finishCallback: Runnable,
) {
TaskViewUtils.composeRecentsSplitLaunchAnimator(
launchingTaskView,
@@ -611,7 +607,7 @@
depthController,
info,
t,
- finishCallback
+ finishCallback,
)
}
@@ -629,7 +625,7 @@
nonApps: Array<RemoteAnimationTarget>,
stateManager: StateManager<*, *>,
depthController: DepthController?,
- finishCallback: Runnable
+ finishCallback: Runnable,
) {
TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
launchingTaskView,
@@ -640,7 +636,7 @@
nonApps,
stateManager,
depthController,
- finishCallback
+ finishCallback,
)
}
@@ -651,7 +647,7 @@
*/
fun hasChangesForBothAppPairs(
launchingIconView: AppPairIcon,
- transitionInfo: TransitionInfo
+ transitionInfo: TransitionInfo,
): Int {
val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName
val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName
@@ -712,7 +708,7 @@
transitionInfo: TransitionInfo,
t: Transaction,
finishCallback: Runnable,
- windowRadius: Float
+ windowRadius: Float,
) {
// If launching an app pair from Taskbar inside of an app context (no access to Launcher),
// use the scale-up animation
@@ -721,7 +717,7 @@
transitionInfo,
t,
finishCallback,
- WINDOWING_MODE_MULTI_WINDOW
+ WINDOWING_MODE_MULTI_WINDOW,
)
return
}
@@ -753,8 +749,7 @@
(!isLeftRightSplit && change.endAbsBounds.top <= 0)
}
val dividerPos =
- if (isLeftRightSplit) leftTopApp.endAbsBounds.right
- else leftTopApp.endAbsBounds.bottom
+ if (isLeftRightSplit) leftTopApp.endAbsBounds.right else leftTopApp.endAbsBounds.bottom
// Create a new floating view in Launcher, positioned above the launching icon
val drawableArea = launchingIconView.iconDrawableArea
@@ -769,7 +764,7 @@
drawableArea,
appIcon1,
appIcon2,
- dividerPos
+ dividerPos,
)
floatingView.bringToFront()
@@ -780,7 +775,7 @@
finishCallback,
launcher,
floatingView,
- mainRootCandidate
+ mainRootCandidate,
)
iconLaunchValueAnimator.addListener(
object : AnimatorListenerAdapter() {
@@ -806,7 +801,7 @@
transitionInfo: TransitionInfo,
t: Transaction,
finishCallback: Runnable,
- launchFullscreenIndex: Int
+ launchFullscreenIndex: Int,
) {
// If launching an app pair from Taskbar inside of an app context (no access to Launcher),
// use the scale-up animation
@@ -815,7 +810,7 @@
transitionInfo,
t,
finishCallback,
- WINDOWING_MODE_FULLSCREEN
+ WINDOWING_MODE_FULLSCREEN,
)
return
}
@@ -867,7 +862,7 @@
drawableArea,
appIcon,
null /*appIcon2*/,
- 0 /*dividerPos*/
+ 0, /*dividerPos*/
)
floatingView.bringToFront()
launchAnimation.play(
@@ -882,7 +877,7 @@
finishCallback: Runnable,
launcher: QuickstepLauncher,
floatingView: FloatingAppPairView,
- rootCandidate: Change
+ rootCandidate: Change,
): ValueAnimator {
val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
@@ -896,7 +891,7 @@
Interpolators.LINEAR,
valueAnimator.animatedFraction,
timings.appRevealStartOffset,
- timings.appRevealEndOffset
+ timings.appRevealEndOffset,
)
// Set the alpha of the shell layer (2 apps + divider)
@@ -913,8 +908,8 @@
Interpolators.clampToProgress(
timings.getStagedRectXInterpolator(),
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
var mDy =
FloatProp(
@@ -923,8 +918,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED,
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
var mScaleX =
FloatProp(
@@ -933,8 +928,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED,
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
var mScaleY =
FloatProp(
@@ -943,8 +938,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED,
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
override fun onUpdate(percent: Float, initOnly: Boolean) {
@@ -979,7 +974,7 @@
transitionInfo: TransitionInfo,
t: Transaction,
finishCallback: Runnable,
- windowingMode: Int
+ windowingMode: Int,
) {
val launchAnimation = AnimatorSet()
val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
@@ -1066,7 +1061,7 @@
transitionInfo: TransitionInfo,
t: Transaction,
finishCallback: Runnable,
- cornerRadius: Float
+ cornerRadius: Float,
) {
var splitRoot1: Change? = null
var splitRoot2: Change? = null
@@ -1131,7 +1126,7 @@
Interpolators.LINEAR,
valueAnimator.animatedFraction,
0.8f,
- 1f
+ 1f,
)
for (leash in openingTargets) {
animTransaction.setAlpha(leash, progress)
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 99bfa7e..a76ebdb 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -3541,11 +3541,6 @@
}
private void setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha) {
- if (enableRefactorTaskThumbnail()) {
- mRecentsViewModel.updateThumbnailSplashProgress(taskThumbnailSplashAlpha);
- return;
- }
-
mTaskThumbnailSplashAlpha = taskThumbnailSplashAlpha;
for (TaskView taskView : getTaskViews()) {
taskView.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index b6f6bed..bbe1af4 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -25,16 +25,14 @@
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.ViewUtils.addAccessibleChildToList
import com.android.quickstep.recents.di.RecentsDependencies
-import com.android.quickstep.recents.di.get
import com.android.quickstep.recents.di.getScope
import com.android.quickstep.recents.di.inject
import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper
import com.android.quickstep.recents.ui.viewmodel.TaskData
-import com.android.quickstep.recents.viewmodel.TaskContainerViewModel
import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
/** Holder for all Task dependent information. */
class TaskContainer(
@@ -56,20 +54,14 @@
taskOverlayFactory: TaskOverlayFactory,
) {
val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
- lateinit var taskContainerData: TaskContainerData
+ // TODO(b/390581380): Remove this after this bug is fixed
private val taskThumbnailViewModel: TaskThumbnailViewModel by
RecentsDependencies.inject(snapshotView)
- // TODO(b/335649589): Ideally create and obtain this from DI.
- private val taskContainerViewModel: TaskContainerViewModel by lazy {
- TaskContainerViewModel(splashAlphaUseCase = RecentsDependencies.get())
- }
-
init {
if (enableRefactorTaskThumbnail()) {
require(snapshotView is TaskThumbnailView)
- taskContainerData = RecentsDependencies.get(this)
RecentsDependencies.getScope(snapshotView).apply {
val taskViewScope = RecentsDependencies.getScope(taskView)
linkTo(taskViewScope)
@@ -82,9 +74,11 @@
}
}
- var splitAnimationThumbnail: Bitmap? = null
- get() = if (enableRefactorTaskThumbnail()) field else thumbnailViewDeprecated.thumbnail
- private set
+ internal var thumbnailData: ThumbnailData? = null
+ val splitAnimationThumbnail: Bitmap?
+ get() =
+ if (enableRefactorTaskThumbnail()) thumbnailData?.thumbnail
+ else thumbnailViewDeprecated.thumbnail
val thumbnailView: TaskThumbnailView
get() {
@@ -98,10 +92,12 @@
return snapshotView as TaskThumbnailViewDeprecated
}
+ var isThumbnailValid: Boolean = false
+ internal set
+
val shouldShowSplashView: Boolean
get() =
- if (enableRefactorTaskThumbnail())
- taskContainerViewModel.shouldShowThumbnailSplash(task.key.id)
+ if (enableRefactorTaskThumbnail()) taskView.shouldShowSplash()
else thumbnailViewDeprecated.shouldShowSplashView()
/** Builds proto for logging */
@@ -111,7 +107,7 @@
fun bind() {
digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition)
if (enableRefactorTaskThumbnail()) {
- bindThumbnailView()
+ taskThumbnailViewModel.bind(task.key.id)
} else {
thumbnailViewDeprecated.bind(task, overlay, taskView)
}
@@ -126,6 +122,9 @@
if (enableRefactorTaskThumbnail()) {
RecentsDependencies.getInstance().removeScope(snapshotView)
RecentsDependencies.getInstance().removeScope(this)
+ isThumbnailValid = false
+ } else {
+ thumbnailViewDeprecated.setShowSplashForSplitSelection(false)
}
}
@@ -134,10 +133,6 @@
thumbnailView.destroyScopes()
}
- private fun bindThumbnailView() {
- taskThumbnailViewModel.bind(task.key.id)
- }
-
fun setOverlayEnabled(enabled: Boolean) {
if (!enableRefactorTaskThumbnail()) {
thumbnailViewDeprecated.setOverlayEnabled(enabled)
@@ -157,15 +152,41 @@
TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile, hasHeader),
state?.taskId,
)
- splitAnimationThumbnail =
- if (state is TaskData.Data) state.thumbnailData?.thumbnail else null
+ thumbnailData = if (state is TaskData.Data) state.thumbnailData else null
}
fun updateTintAmount(tintAmount: Float) {
thumbnailView.updateTintAmount(tintAmount)
}
+ /**
+ * Updates the progress of the menu opening animation.
+ *
+ * This function propagates the given `progress` value to the `thumbnailView` allowing the
+ * thumbnail view to animate its visual state in sync with the menu's opening/closing
+ * transition.
+ *
+ * @param progress The progress of the menu opening animation (from closed=0 to fully open=1)
+ */
fun updateMenuOpenProgress(progress: Float) {
thumbnailView.updateMenuOpenProgress(progress)
}
+
+ /**
+ * Updates the thumbnail splash progress for a given task.
+ *
+ * This function manages the visual feedback of a "splash" effect that can be displayed over a
+ * thumbnail image, typically during loading or updating. It calculates the alpha (transparency)
+ * of the splash based on the provided progress and then applies this alpha to the thumbnail
+ * view if it should be displayed.
+ *
+ * @param progress The progress of the operation, ranging from 0.0 to 1.0
+ */
+ fun updateThumbnailSplashProgress(progress: Float) {
+ if (enableRefactorTaskThumbnail()) {
+ thumbnailView.updateSplashAlpha(progress)
+ } else {
+ thumbnailViewDeprecated.setSplashAlpha(progress)
+ }
+ }
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 0465dbc..5093259 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -41,6 +41,7 @@
import android.widget.Toast
import androidx.annotation.IntDef
import androidx.annotation.VisibleForTesting
+import androidx.core.view.doOnLayout
import androidx.core.view.updateLayoutParams
import com.android.app.animation.Interpolators
import com.android.launcher3.Flags.enableCursorHoverStates
@@ -330,6 +331,12 @@
onModalnessUpdated(field)
}
+ var splitSplashAlpha = 0f
+ set(value) {
+ field = value
+ applyThumbnailSplashAlpha()
+ }
+
protected var taskThumbnailSplashAlpha = 0f
set(value) {
field = value
@@ -647,6 +654,8 @@
viewModel = null
attachAlpha = 1f
splitAlpha = 1f
+ splitSplashAlpha = 0f
+ taskThumbnailSplashAlpha = 0f
// Clear any references to the thumbnail (it will be re-read either from the cache or the
// system on next bind)
if (!enableRefactorTaskThumbnail()) {
@@ -769,9 +778,20 @@
liveTile = state.isLiveTile,
hasHeader = type == TaskViewType.DESKTOP,
)
+ updateThumbnailValidity(container)
}
}
+ private fun updateThumbnailValidity(container: TaskContainer) {
+ container.isThumbnailValid =
+ viewModel!!.isThumbnailValid(
+ thumbnail = container.thumbnailData,
+ width = container.thumbnailView.width,
+ height = container.thumbnailView.height,
+ )
+ applyThumbnailSplashAlpha()
+ }
+
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
if (enableRefactorTaskThumbnail()) {
@@ -808,7 +828,7 @@
onBind(orientedState)
}
- open fun onBind(orientedState: RecentsOrientedState) {
+ protected open fun onBind(orientedState: RecentsOrientedState) {
if (enableRefactorTaskThumbnail()) {
viewModel =
TaskViewModel(
@@ -816,20 +836,37 @@
recentsViewData = RecentsDependencies.get(),
getTaskUseCase = RecentsDependencies.get(),
getSysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
+ isThumbnailValidUseCase = RecentsDependencies.get(),
dispatcherProvider = RecentsDependencies.get(),
)
.apply { bind(*taskIds) }
}
- taskContainers.forEach {
- it.bind()
+ taskContainers.forEach { container ->
+ container.bind()
if (enableRefactorTaskThumbnail()) {
- it.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
+ container.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
+ container.thumbnailView.doOnLayout { updateThumbnailValidity(container) }
}
}
setOrientationState(orientedState)
}
+ private fun applyThumbnailSplashAlpha() {
+ val alpha = getSplashAlphaProgress()
+ taskContainers.forEach { it.updateThumbnailSplashProgress(alpha) }
+ }
+
+ private fun getSplashAlphaProgress(): Float =
+ when {
+ !enableRefactorTaskThumbnail() -> taskThumbnailSplashAlpha
+ splitSplashAlpha > 0f -> splitSplashAlpha
+ shouldShowSplash() -> taskThumbnailSplashAlpha
+ else -> 0f
+ }
+
+ internal fun shouldShowSplash(): Boolean = taskContainers.any { !it.isThumbnailValid }
+
protected fun createTaskContainer(
task: Task,
@IdRes thumbnailViewId: Int,
@@ -1295,6 +1332,7 @@
if (isQuickSwitch) {
setFreezeRecentTasksReordering()
}
+ // TODO(b/331754864): Update this to use TV.shouldShowSplash
disableStartingWindow = firstTaskContainer.shouldShowSplashView
}
Executors.UI_HELPER_EXECUTOR.execute {
@@ -1585,14 +1623,6 @@
updateFullscreenParams()
}
- protected open fun applyThumbnailSplashAlpha() {
- if (!enableRefactorTaskThumbnail()) {
- taskContainers.forEach {
- it.thumbnailViewDeprecated.setSplashAlpha(taskThumbnailSplashAlpha)
- }
- }
- }
-
private fun applyTranslationX() {
translationX =
dismissTranslationX +
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
index 3e0c186..1a2b1c3 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
@@ -18,11 +18,8 @@
import android.graphics.Matrix
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
- override val splashAlpha = MutableStateFlow(0f)
-
override fun bind(taskId: Int) {
// no-op
}
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 356080a..232a08a 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
@@ -125,7 +125,6 @@
as TaskThumbnailView
taskThumbnailView.cornerRadius = CORNER_RADIUS
val ttvDiScopeId = di.getScope(taskThumbnailView).scopeId
- di.provide(TaskThumbnailViewData::class.java, ttvDiScopeId) { TaskThumbnailViewData() }
di.provide(TaskThumbnailViewModel::class.java, ttvDiScopeId) { taskThumbnailViewModel }
return taskThumbnailView
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt
new file mode 100644
index 0000000..e8bca93
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.recents.domain.usecase
+
+import android.graphics.Bitmap
+import android.view.Surface
+import android.view.Surface.ROTATION_90
+import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class IsThumbnailValidUseCaseTest {
+ private val recentsRotationStateRepository = FakeRecentsRotationStateRepository()
+ private val systemUnderTest = IsThumbnailValidUseCase(recentsRotationStateRepository)
+
+ @Test
+ fun withNullThumbnail_returnsInvalid() = runTest {
+ val isThumbnailValid = systemUnderTest(thumbnailData = null, viewWidth = 0, viewHeight = 0)
+ assertThat(isThumbnailValid).isEqualTo(false)
+ }
+
+ @Test
+ fun sameAspectRatio_sameRotation_returnsValid() = runTest {
+ val isThumbnailValid =
+ systemUnderTest.invoke(
+ thumbnailData = createThumbnailData(),
+ viewWidth = THUMBNAIL_WIDTH * 2,
+ viewHeight = THUMBNAIL_HEIGHT * 2,
+ )
+ assertThat(isThumbnailValid).isEqualTo(true)
+ }
+
+ @Test
+ fun differentAspectRatio_sameRotation_returnsInvalid() = runTest {
+ val isThumbnailValid =
+ systemUnderTest.invoke(
+ thumbnailData = createThumbnailData(),
+ viewWidth = THUMBNAIL_WIDTH,
+ viewHeight = THUMBNAIL_HEIGHT * 2,
+ )
+ assertThat(isThumbnailValid).isEqualTo(false)
+ }
+
+ @Test
+ fun sameAspectRatio_differentRotation_returnsInvalid() = runTest {
+ val isThumbnailValid =
+ systemUnderTest.invoke(
+ thumbnailData = createThumbnailData(rotation = ROTATION_90),
+ viewWidth = THUMBNAIL_WIDTH * 2,
+ viewHeight = THUMBNAIL_HEIGHT * 2,
+ )
+ assertThat(isThumbnailValid).isEqualTo(false)
+ }
+
+ @Test
+ fun differentAspectRatio_differentRotation_returnsInvalid() = runTest {
+ val isThumbnailValid =
+ systemUnderTest.invoke(
+ thumbnailData = createThumbnailData(rotation = ROTATION_90),
+ viewWidth = THUMBNAIL_WIDTH,
+ viewHeight = THUMBNAIL_HEIGHT * 2,
+ )
+ assertThat(isThumbnailValid).isEqualTo(false)
+ }
+
+ private fun createThumbnailData(
+ rotation: Int = Surface.ROTATION_0,
+ width: Int = THUMBNAIL_WIDTH,
+ height: Int = THUMBNAIL_HEIGHT,
+ ): ThumbnailData {
+ val bitmap = mock<Bitmap>()
+ whenever(bitmap.width).thenReturn(width)
+ whenever(bitmap.height).thenReturn(height)
+ return ThumbnailData(thumbnail = bitmap, rotation = rotation)
+ }
+
+ companion object {
+ const val THUMBNAIL_WIDTH = 100
+ const val THUMBNAIL_HEIGHT = 200
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
index c031150..08e459b 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
@@ -25,9 +25,11 @@
import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
import com.android.launcher3.util.TestDispatcherProvider
+import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
import com.android.quickstep.recents.domain.model.TaskModel
import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -41,6 +43,10 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@@ -52,6 +58,8 @@
private val recentsViewData = RecentsViewData()
private val getTaskUseCase = mock<GetTaskUseCase>()
+ private val isThumbnailValidUseCase =
+ spy(IsThumbnailValidUseCase(FakeRecentsRotationStateRepository()))
private lateinit var sut: TaskViewModel
@Before
@@ -62,6 +70,7 @@
recentsViewData = recentsViewData,
getTaskUseCase = getTaskUseCase,
getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
+ isThumbnailValidUseCase = isThumbnailValidUseCase,
dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
)
whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) })
@@ -102,6 +111,7 @@
recentsViewData = recentsViewData,
getTaskUseCase = getTaskUseCase,
getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
+ isThumbnailValidUseCase = isThumbnailValidUseCase,
dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
)
sut.bind(TASK_MODEL_1.id)
@@ -225,6 +235,12 @@
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
+ @Test
+ fun shouldShowSplash_calls_useCase() {
+ sut.isThumbnailValid(null, 0, 0)
+ verify(isThumbnailValidUseCase).invoke(anyOrNull(), anyInt(), anyInt())
+ }
+
private fun TaskModel.toUiState() =
TaskData.Data(
taskId = id,
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
deleted file mode 100644
index 0767fb9..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
+++ /dev/null
@@ -1,149 +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.task.thumbnail
-
-import android.content.ComponentName
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.graphics.drawable.Drawable
-import android.view.Surface
-import android.view.Surface.ROTATION_90
-import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.viewmodel.TaskContainerData
-import com.android.systemui.shared.recents.model.Task
-import com.android.systemui.shared.recents.model.ThumbnailData
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-class SplashAlphaUseCaseTest {
- private val recentsViewData = RecentsViewData()
- private val taskContainerData = TaskContainerData()
- private val taskThumbnailViewData = TaskThumbnailViewData()
- private val recentTasksRepository = FakeTasksRepository()
- private val recentsRotationStateRepository = FakeRecentsRotationStateRepository()
- private val systemUnderTest =
- SplashAlphaUseCase(
- recentsViewData,
- taskContainerData,
- taskThumbnailViewData,
- recentTasksRepository,
- recentsRotationStateRepository,
- )
-
- @Test
- fun execute_withNullThumbnail_showsSplash() = runTest {
- assertThat(systemUnderTest.execute(0).first()).isEqualTo(SPLASH_HIDDEN)
- }
-
- @Test
- fun execute_withTaskSpecificSplashAlpha_showsSplash() = runTest {
- setupTask(2)
- taskContainerData.thumbnailSplashProgress.value = 0.7f
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.7f)
- }
-
- @Test
- fun execute_withNoGlobalSplashEnabled_doesntShowSplash() = runTest {
- setupTask(2)
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
- }
-
- @Test
- fun execute_withSameAspectRatioAndRotation_withGlobalSplashEnabled_doesntShowSplash() =
- runTest {
- setupTask(2)
- recentsViewData.thumbnailSplashProgress.value = 0.5f
- taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
- taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
- }
-
- @Test
- fun execute_withDifferentAspectRatioAndSameRotation_showsSplash() = runTest {
- setupTask(2)
- recentsViewData.thumbnailSplashProgress.value = 0.5f
- taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
- taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
- }
-
- @Test
- fun execute_withSameAspectRatioAndDifferentRotation_showsSplash() = runTest {
- setupTask(2, createThumbnailData(rotation = ROTATION_90))
- recentsViewData.thumbnailSplashProgress.value = 0.5f
- taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
- taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
- }
-
- @Test
- fun execute_withDifferentAspectRatioAndRotation_showsSplash() = runTest {
- setupTask(2, createThumbnailData(rotation = ROTATION_90))
- recentsViewData.thumbnailSplashProgress.value = 0.5f
- taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
- taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
- }
-
- private val tasks = (0..5).map(::createTaskWithId)
-
- private fun setupTask(taskId: Int, thumbnailData: ThumbnailData = createThumbnailData()) {
- recentTasksRepository.seedThumbnailData(mapOf(taskId to thumbnailData))
- val expectedIconData = mock<Drawable>()
- recentTasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
- recentTasksRepository.seedTasks(tasks)
- recentTasksRepository.setVisibleTasks(setOf(taskId))
- }
-
- private fun createThumbnailData(
- rotation: Int = Surface.ROTATION_0,
- width: Int = THUMBNAIL_WIDTH,
- height: Int = THUMBNAIL_HEIGHT,
- ): ThumbnailData {
- val bitmap = mock<Bitmap>()
- whenever(bitmap.width).thenReturn(width)
- whenever(bitmap.height).thenReturn(height)
-
- return ThumbnailData(thumbnail = bitmap, rotation = rotation)
- }
-
- private fun createTaskWithId(taskId: Int) =
- Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
- colorBackground = Color.argb(taskId, taskId, taskId, taskId)
- }
-
- companion object {
- const val THUMBNAIL_WIDTH = 100
- const val THUMBNAIL_HEIGHT = 200
-
- const val SPLASH_HIDDEN = 0f
- const val SPLASH_SHOWN = 1f
- }
-}
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 aec586d..4b4e2eb 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
@@ -19,7 +19,6 @@
import android.graphics.Matrix
import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.launcher3.util.TestDispatcherProvider
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
@@ -42,17 +41,9 @@
private val dispatcher = StandardTestDispatcher()
private val testScope = TestScope(dispatcher)
- private val dispatcherProvider = TestDispatcherProvider(dispatcher)
private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
- private val splashAlphaUseCase: SplashAlphaUseCase = mock()
- private val systemUnderTest by lazy {
- TaskThumbnailViewModelImpl(
- dispatcherProvider,
- mGetThumbnailPositionUseCase,
- splashAlphaUseCase,
- )
- }
+ private val systemUnderTest by lazy { TaskThumbnailViewModelImpl(mGetThumbnailPositionUseCase) }
@Test
fun getSnapshotMatrix_MissingThumbnail() =