Merge "Rename OverviewProxyService to LauncherProxyService" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 927254d..2e48910 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -40,8 +40,8 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISMISS_IME;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_ALT_BACK;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
@@ -134,8 +134,14 @@
private static final int FLAG_IME_SWITCHER_BUTTON_VISIBLE = 1 << 0;
/** Whether the IME is visible. */
private static final int FLAG_IME_VISIBLE = 1 << 1;
- /** Whether the back button is adjusted for the IME. */
- private static final int FLAG_IME_ALT_BACK = 1 << 2;
+ /**
+ * The back button is visually adjusted to indicate that it will dismiss the IME when pressed.
+ * This only takes effect while the IME is visible. By default, it is set while the IME is
+ * visible, but may be overridden by the
+ * {@link android.inputmethodservice.InputMethodService.BackDispositionMode backDispositionMode}
+ * set by the IME.
+ */
+ private static final int FLAG_BACK_DISMISS_IME = 1 << 2;
private static final int FLAG_A11Y_VISIBLE = 1 << 3;
private static final int FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE = 1 << 4;
private static final int FLAG_KEYGUARD_VISIBLE = 1 << 5;
@@ -277,13 +283,14 @@
}
protected void setupController() {
- boolean isThreeButtonNav = mContext.isThreeButtonNav();
+ final boolean isThreeButtonNav = mContext.isThreeButtonNav();
+ final boolean isPhoneMode = mContext.isPhoneMode();
DeviceProfile deviceProfile = mContext.getDeviceProfile();
Resources resources = mContext.getResources();
int setupSize = mControllers.taskbarActivityContext.getSetupWindowSize();
- Point p = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources,
- mContext.isPhoneMode(), mContext.isGestureNav());
+ Point p = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources, isPhoneMode,
+ mContext.isGestureNav());
ViewGroup.LayoutParams navButtonsViewLayoutParams = mNavButtonsView.getLayoutParams();
navButtonsViewLayoutParams.width = p.x;
if (!mContext.isUserSetupComplete()) {
@@ -304,8 +311,10 @@
mImeSwitcherButton = addButton(switcherResId, BUTTON_IME_SWITCH,
isThreeButtonNav ? mStartContextualContainer : mEndContextualContainer,
mControllers.navButtonController, R.id.ime_switcher);
+ // A11y and IME Switcher buttons overlap on phone mode, show only a11y if both visible.
mPropertyHolders.add(new StatePropertyHolder(mImeSwitcherButton,
- flags -> ((flags & FLAG_IME_SWITCHER_BUTTON_VISIBLE) != 0)));
+ flags -> (flags & FLAG_IME_SWITCHER_BUTTON_VISIBLE) != 0
+ && !(isPhoneMode && (flags & FLAG_A11Y_VISIBLE) != 0)));
}
mPropertyHolders.add(new StatePropertyHolder(
@@ -319,7 +328,7 @@
.get(ALPHA_INDEX_SMALL_SCREEN),
flags -> (flags & FLAG_SMALL_SCREEN) == 0));
- if (!mContext.isPhoneMode()) {
+ if (!isPhoneMode) {
mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController
.getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0));
}
@@ -337,7 +346,7 @@
// - IME is visible (add separate translation for IME)
// - VoiceInteractionWindow (assistant) is showing
// - Keyboard shortcuts helper is showing
- if (!mContext.isPhoneMode()) {
+ if (!isPhoneMode) {
int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE
| FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING;
mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui,
@@ -365,9 +374,9 @@
initButtons(mNavButtonContainer, mEndContextualContainer,
mControllers.navButtonController);
updateButtonLayoutSpacing();
- updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneMode());
+ updateStateForFlag(FLAG_SMALL_SCREEN, isPhoneMode);
- if (!mContext.isPhoneMode()) {
+ if (!isPhoneMode) {
mPropertyHolders.add(new StatePropertyHolder(
mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0));
@@ -394,7 +403,7 @@
R.bool.floating_rotation_button_position_left);
mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton,
mRotationButtonListener);
- if (mContext.isPhoneMode()) {
+ if (isPhoneMode) {
mTaskbarTransitions.init();
}
@@ -450,7 +459,7 @@
flags -> (flags & FLAG_IME_VISIBLE) == 0));
}
mPropertyHolders.add(new StatePropertyHolder(mBackButton,
- flags -> (flags & FLAG_IME_ALT_BACK) != 0,
+ flags -> (flags & FLAG_BACK_DISMISS_IME) != 0,
ROTATION_DRAWABLE_PERCENT, 1f, 0f));
// Translate back button to be at end/start of other buttons for keyguard (only after SUW
// since it is laid to align with SUW actions while in that state)
@@ -512,7 +521,7 @@
boolean isImeSwitcherButtonVisible =
(sysUiStateFlags & SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE) != 0;
boolean isImeVisible = (sysUiStateFlags & SYSUI_STATE_IME_VISIBLE) != 0;
- boolean useImeAltBack = (sysUiStateFlags & SYSUI_STATE_IME_ALT_BACK) != 0;
+ boolean isBackDismissIme = (sysUiStateFlags & SYSUI_STATE_BACK_DISMISS_IME) != 0;
boolean a11yVisible = (sysUiStateFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
boolean isHomeDisabled = (sysUiStateFlags & SYSUI_STATE_HOME_DISABLED) != 0;
boolean isRecentsDisabled = (sysUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0;
@@ -528,7 +537,7 @@
updateStateForFlag(FLAG_IME_SWITCHER_BUTTON_VISIBLE, isImeSwitcherButtonVisible);
updateStateForFlag(FLAG_IME_VISIBLE, isImeVisible);
- updateStateForFlag(FLAG_IME_ALT_BACK, useImeAltBack);
+ updateStateForFlag(FLAG_BACK_DISMISS_IME, isBackDismissIme);
updateStateForFlag(FLAG_A11Y_VISIBLE, a11yVisible);
updateStateForFlag(FLAG_DISABLE_HOME, isHomeDisabled);
updateStateForFlag(FLAG_DISABLE_RECENTS, isRecentsDisabled);
@@ -1233,7 +1242,7 @@
appendFlag(str, flags, FLAG_IME_SWITCHER_BUTTON_VISIBLE,
"FLAG_IME_SWITCHER_BUTTON_VISIBLE");
appendFlag(str, flags, FLAG_IME_VISIBLE, "FLAG_IME_VISIBLE");
- appendFlag(str, flags, FLAG_IME_ALT_BACK, "FLAG_IME_ALT_BACK");
+ appendFlag(str, flags, FLAG_BACK_DISMISS_IME, "FLAG_BACK_DISMISS_IME");
appendFlag(str, flags, FLAG_A11Y_VISIBLE, "FLAG_A11Y_VISIBLE");
appendFlag(str, flags, FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE,
"FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE");
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index e8a0c45..a109a40 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -43,6 +43,7 @@
import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
+import static com.android.window.flags.Flags.enableStartLaunchTransitionFromTaskbarBugfix;
import static com.android.wm.shell.Flags.enableTinyTaskbar;
import static java.lang.invoke.MethodHandles.Lookup.PROTECTED;
@@ -1689,7 +1690,11 @@
}
// There is no task associated with this launch - launch a new task through an intent
ActivityOptionsWrapper opts = getActivityLaunchDesktopOptions();
- mSysUiProxy.startLaunchIntentTransition(intent, opts.options.toBundle(), displayId);
+ if (enableStartLaunchTransitionFromTaskbarBugfix()) {
+ mSysUiProxy.startLaunchIntentTransition(intent, opts.options.toBundle(), displayId);
+ } else {
+ startActivity(intent, opts.options.toBundle());
+ }
}
/** Expands a folder icon when it is clicked */
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 2f95413..002a4e8 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -17,7 +17,6 @@
package com.android.quickstep.recents.data
import android.graphics.drawable.Drawable
-import android.graphics.drawable.ShapeDrawable
import android.util.Log
import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
@@ -52,37 +51,29 @@
override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> {
if (forceRefresh) {
recentsModel.getTasks { newTaskList ->
- val oldTaskMap = tasks.value
val recentTasks =
newTaskList
.flatMap { groupTask -> groupTask.tasks }
.associateBy { it.key.id }
.also { newTaskMap ->
// Clean tasks that are not in the latest group tasks list.
- val tasksNoLongerVisible = oldTaskMap.keys.subtract(newTaskMap.keys)
+ val tasksNoLongerVisible = tasks.value.keys.subtract(newTaskMap.keys)
removeTasks(tasksNoLongerVisible)
-
- // Use pre-loaded thumbnail data and icon from the previous list.
- // This reduces the Thumbnail loading time in the Overview and prevent
- // empty thumbnail and icon.
- val cache =
- taskRequests.keys
- .mapNotNull { key ->
- val task = oldTaskMap[key] ?: return@mapNotNull null
- key to Pair(task.thumbnail, task.icon)
- }
- .toMap()
-
- newTaskMap.values.forEach { task ->
- task.thumbnail = task.thumbnail ?: cache[task.key.id]?.first
- task.icon = task.icon ?: cache[task.key.id]?.second
- }
}
- tasks.value = MapForStateFlow(recentTasks)
Log.d(
TAG,
- "getAllTaskData: oldTasks ${oldTaskMap.keys}, newTasks: ${recentTasks.keys}",
+ "getAllTaskData: oldTasks ${tasks.value.keys}, newTasks: ${recentTasks.keys}",
)
+ tasks.value = MapForStateFlow(recentTasks)
+
+ // Request data for completed tasks to prevent stale data.
+ // This will prevent thumbnail and icon from being replaced and
+ // null due to race condition.
+ taskRequests.values.forEach { (taskKey, job) ->
+ if (job.isCompleted) {
+ requestTaskData(taskKey.id)
+ }
+ }
}
}
return tasks.map { it.values.toList() }
@@ -202,13 +193,11 @@
private suspend fun getIconFromDataSource(task: Task) =
withContext(dispatcherProvider.background) {
val iconCacheEntry = taskIconDataSource.getIcon(task)
- val icon = iconCacheEntry.icon.constantState?.newDrawable()?.mutate() ?: EMPTY_DRAWABLE
- IconData(icon, iconCacheEntry.contentDescription, iconCacheEntry.title)
+ IconData(iconCacheEntry.icon, iconCacheEntry.contentDescription, iconCacheEntry.title)
}
companion object {
private const val TAG = "TasksRepository"
- private val EMPTY_DRAWABLE = ShapeDrawable()
}
/** Helper class to support StateFlow emissions when using a Map with a MutableStateFlow. */
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 358537c..0a47338 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -26,6 +26,7 @@
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate
import com.android.quickstep.recents.data.TaskVisualsChangedDelegateImpl
import com.android.quickstep.recents.data.TasksRepository
+import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.GetThumbnailUseCase
import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
@@ -181,8 +182,6 @@
taskContainerData = inject(scopeId),
dispatcherProvider = inject(),
getThumbnailPositionUseCase = inject(),
- tasksRepository = inject(),
- deviceProfileRepository = inject(),
splashAlphaUseCase = inject(scopeId),
)
TaskOverlayViewModel::class.java -> {
@@ -195,6 +194,7 @@
dispatcherProvider = inject(),
)
}
+ GetTaskUseCase::class.java -> GetTaskUseCase(repository = inject())
GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
SysUiStatusNavFlagsUseCase::class.java ->
SysUiStatusNavFlagsUseCase(taskRepository = inject())
diff --git a/quickstep/src/com/android/quickstep/recents/domain/model/TaskModel.kt b/quickstep/src/com/android/quickstep/recents/domain/model/TaskModel.kt
index 3823100..bf29b1d 100644
--- a/quickstep/src/com/android/quickstep/recents/domain/model/TaskModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/domain/model/TaskModel.kt
@@ -38,7 +38,7 @@
*/
data class TaskModel(
val id: TaskId,
- val title: String,
+ val title: String?,
val titleDescription: String?,
val icon: Drawable?,
val thumbnail: ThumbnailData?,
diff --git a/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt b/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt
new file mode 100644
index 0000000..fb62268
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.ui.mapper
+
+import com.android.quickstep.recents.ui.viewmodel.TaskData
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+
+object TaskUiStateMapper {
+
+ /**
+ * Converts a [TaskData] object into a [TaskThumbnailUiState] for display in the UI.
+ *
+ * This function handles different types of [TaskData] and determines the appropriate UI state
+ * based on the data and provided flags.
+ *
+ * @param taskData The [TaskData] to convert. Can be null or a specific subclass.
+ * @param isLiveTile A flag indicating whether the task data represents live tile.
+ * @param hasHeader A flag indicating whether the UI should display a header.
+ * @return A [TaskThumbnailUiState] representing the UI state for the given task data.
+ */
+ fun toTaskThumbnailUiState(
+ taskData: TaskData?,
+ isLiveTile: Boolean,
+ hasHeader: Boolean,
+ ): TaskThumbnailUiState =
+ when {
+ taskData !is TaskData.Data -> Uninitialized
+ isLiveTile -> createLiveTileState(taskData, hasHeader)
+ isBackgroundOnly(taskData) -> BackgroundOnly(taskData.backgroundColor)
+ isSnapshotSplash(taskData) ->
+ SnapshotSplash(createSnapshotState(taskData, hasHeader), taskData.icon)
+ else -> Uninitialized
+ }
+
+ private fun createSnapshotState(taskData: TaskData.Data, hasHeader: Boolean): Snapshot =
+ if (canHeaderBeCreated(taskData, hasHeader)) {
+ Snapshot.WithHeader(
+ taskData.thumbnailData?.thumbnail!!,
+ taskData.thumbnailData.rotation,
+ taskData.backgroundColor,
+ ThumbnailHeader(taskData.icon!!, taskData.titleDescription!!),
+ )
+ } else {
+ Snapshot.WithoutHeader(
+ taskData.thumbnailData?.thumbnail!!,
+ taskData.thumbnailData.rotation,
+ taskData.backgroundColor,
+ )
+ }
+
+ private fun isBackgroundOnly(taskData: TaskData.Data) =
+ taskData.isLocked || taskData.thumbnailData == null
+
+ private fun isSnapshotSplash(taskData: TaskData.Data) =
+ taskData.thumbnailData?.thumbnail != null && !taskData.isLocked
+
+ private fun canHeaderBeCreated(taskData: TaskData.Data, hasHeader: Boolean) =
+ hasHeader && taskData.icon != null && taskData.titleDescription != null
+
+ private fun createLiveTileState(taskData: TaskData.Data, hasHeader: Boolean) =
+ if (canHeaderBeCreated(taskData, hasHeader)) {
+ // TODO(http://b/353965691): figure out what to do when `icon` or `titleDescription` is
+ // null.
+ LiveTile.WithHeader(ThumbnailHeader(taskData.icon!!, taskData.titleDescription!!))
+ } else LiveTile.WithoutHeader
+}
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt
index 5f98479..54b2389 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt
@@ -30,28 +30,36 @@
* @property isLiveTile Indicates whether this data is intended for a live tile. If `true`, the
* running app will be displayed instead of the thumbnail.
*/
-data class TaskTileUiState(val tasks: List<TaskData>, val isLiveTile: Boolean)
+data class TaskTileUiState(
+ val tasks: List<TaskData>,
+ val isLiveTile: Boolean,
+ val hasHeader: Boolean,
+)
-sealed interface TaskData {
+sealed class TaskData {
+ abstract val taskId: Int
+
/** When no data was found for the TaskId provided */
- data class NoData(val taskId: Int) : TaskData
+ data class NoData(override val taskId: Int) : TaskData()
/**
* This class provides UI information related to a Task (App) to be displayed within a TaskView.
*
* @property taskId Identifier of the task
* @property title App title
+ * @property titleDescription App content description
* @property icon App icon
* @property thumbnailData Information related to the last snapshot retrieved from the app
* @property backgroundColor The background color of the task.
* @property isLocked Indicates whether the task is locked or not.
*/
data class Data(
- val taskId: Int,
- val title: String,
+ override val taskId: Int,
+ val title: String?,
+ val titleDescription: String?,
val icon: Drawable?,
val thumbnailData: ThumbnailData?,
val backgroundColor: Int,
val isLocked: Boolean,
- ) : TaskData
+ ) : TaskData()
}
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 2e51a8a..b2806f0 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
@@ -16,12 +16,15 @@
package com.android.quickstep.recents.ui.viewmodel
+import android.annotation.ColorInt
import android.util.Log
+import androidx.core.graphics.ColorUtils
import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.domain.model.TaskId
import com.android.quickstep.recents.domain.model.TaskModel
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.views.TaskViewType
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -37,6 +40,7 @@
*/
@OptIn(ExperimentalCoroutinesApi::class)
class TaskViewModel(
+ private val taskViewType: TaskViewType,
recentsViewData: RecentsViewData,
private val getTaskUseCase: GetTaskUseCase,
dispatcherProvider: DispatcherProvider,
@@ -62,7 +66,14 @@
::mapToUiState,
)
}
- .combine(isLiveTile) { tasks, isLiveTile -> TaskTileUiState(tasks, isLiveTile) }
+ .combine(isLiveTile) { tasks, isLiveTile ->
+ TaskTileUiState(
+ tasks = tasks,
+ isLiveTile = isLiveTile,
+ hasHeader = taskViewType == TaskViewType.DESKTOP,
+ )
+ }
+ .distinctUntilChanged()
.flowOn(dispatcherProvider.background)
fun bind(vararg taskId: TaskId) {
@@ -78,13 +89,16 @@
TaskData.Data(
taskId = taskId,
title = result.title,
+ titleDescription = result.titleDescription,
icon = result.icon,
thumbnailData = result.thumbnail,
- backgroundColor = result.backgroundColor,
+ backgroundColor = result.backgroundColor.removeAlpha(),
isLocked = result.isLocked,
)
} ?: TaskData.NoData(taskId)
+ @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
+
private companion object {
const val TAG = "TaskViewModel"
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 02baa39..4a990b3 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -20,6 +20,7 @@
import android.graphics.Color
import android.graphics.Outline
import android.graphics.Rect
+import android.graphics.drawable.ShapeDrawable
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
@@ -108,21 +109,6 @@
viewData = RecentsDependencies.get(this)
updateViewDataValues()
viewModel = RecentsDependencies.get(this)
- viewModel.uiState
- .dropWhile { it == Uninitialized }
- .flowOn(dispatcherProvider.background)
- .onEach { viewModelUiState ->
- Log.d(TAG, "viewModelUiState changed from: $uiState to: $viewModelUiState")
- uiState = viewModelUiState
- resetViews()
- when (viewModelUiState) {
- is Uninitialized -> {}
- is LiveTile -> drawLiveWindow(viewModelUiState)
- is SnapshotSplash -> drawSnapshotSplash(viewModelUiState)
- is BackgroundOnly -> drawBackground(viewModelUiState.backgroundColor)
- }
- }
- .launchIn(viewAttachedScope)
viewModel.dimProgress
.dropWhile { it == 0f }
.flowOn(dispatcherProvider.background)
@@ -166,6 +152,19 @@
}
}
+ fun setState(state: TaskThumbnailUiState) {
+ Log.d(TAG, "viewModelUiState changed from: $uiState to: $state")
+ if (uiState == state) return
+ uiState = state
+ resetViews()
+ when (state) {
+ is Uninitialized -> {}
+ is LiveTile -> drawLiveWindow(state)
+ is SnapshotSplash -> drawSnapshotSplash(state)
+ is BackgroundOnly -> drawBackground(state.backgroundColor)
+ }
+ }
+
private fun updateViewDataValues() {
viewData.width.value = width
viewData.height.value = height
@@ -219,7 +218,8 @@
drawSnapshot(snapshotSplash.snapshot)
splashBackground.setBackgroundColor(snapshotSplash.snapshot.backgroundColor)
- splashIcon.setImageDrawable(snapshotSplash.splash)
+ val icon = snapshotSplash.splash?.constantState?.newDrawable()?.mutate() ?: ShapeDrawable()
+ splashIcon.setImageDrawable(icon)
}
private fun drawSnapshot(snapshot: Snapshot) {
@@ -238,10 +238,6 @@
thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
}
- private companion object {
- const val TAG = "TaskThumbnailView"
- }
-
private fun maybeCreateHeader() {
if (enableDesktopExplodedView() && taskThumbnailViewHeader == null) {
taskThumbnailViewHeader =
@@ -251,4 +247,8 @@
addView(taskThumbnailViewHeader)
}
}
+
+ private companion object {
+ const val TAG = "TaskThumbnailView"
+ }
}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
index a048a1d..a9fdaa5 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -17,7 +17,6 @@
package com.android.quickstep.task.viewmodel
import android.graphics.Matrix
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
import kotlinx.coroutines.flow.Flow
/** ViewModel for representing TaskThumbnails */
@@ -28,9 +27,6 @@
/** Provides the alpha of the splash icon */
val splashAlpha: Flow<Float>
- /** Provides the UiState by which the task thumbnail can be represented */
- val uiState: Flow<TaskThumbnailUiState>
-
/** 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 a154c3c..4e4e225 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -16,50 +16,31 @@
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
import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
-import com.android.systemui.shared.recents.model.Task
import kotlin.math.max
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
@OptIn(ExperimentalCoroutinesApi::class)
class TaskThumbnailViewModelImpl(
recentsViewData: RecentsViewData,
taskContainerData: TaskContainerData,
dispatcherProvider: DispatcherProvider,
- private val tasksRepository: RecentTasksRepository,
- private val deviceProfileRepository: RecentsDeviceProfileRepository,
private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
private val splashAlphaUseCase: SplashAlphaUseCase,
) : TaskThumbnailViewModel {
- private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
private val splashProgress = MutableStateFlow(flowOf(0f))
private var taskId: Int = INVALID_TASK_ID
@@ -74,42 +55,9 @@
override val splashAlpha =
splashProgress.flatMapLatest { it }.flowOn(dispatcherProvider.background)
- private val isLiveTile =
- combine(
- task.flatMapLatest { it }.map { it?.key?.id }.distinctUntilChanged(),
- recentsViewData.runningTaskIds,
- recentsViewData.runningTaskShowScreenshot,
- ) { taskId, runningTaskIds, runningTaskShowScreenshot ->
- runningTaskIds.contains(taskId) && !runningTaskShowScreenshot
- }
- .distinctUntilChanged()
-
- override val uiState: Flow<TaskThumbnailUiState> =
- combine(task.flatMapLatest { it }, isLiveTile) { taskVal, isRunning ->
- // TODO(b/369339561) This log is firing a lot. Reduce emissions from TasksRepository
- // then re-enable this log.
- // Log.d(
- // TAG,
- // "Received task and / or live tile update. taskVal: $taskVal"
- // + " isRunning: $isRunning.",
- // )
- when {
- taskVal == null -> Uninitialized
- isRunning -> createLiveTileState(taskVal)
- isBackgroundOnly(taskVal) ->
- BackgroundOnly(taskVal.colorBackground.removeAlpha())
- isSnapshotSplashState(taskVal) ->
- SnapshotSplash(createSnapshotState(taskVal), taskVal.icon)
- else -> Uninitialized
- }
- }
- .distinctUntilChanged()
- .flowOn(dispatcherProvider.background)
-
override fun bind(taskId: Int) {
Log.d(TAG, "bind taskId: $taskId")
this.taskId = taskId
- task.value = tasksRepository.getTaskDataById(taskId)
splashProgress.value = splashAlphaUseCase.execute(taskId)
}
@@ -122,62 +70,6 @@
is ThumbnailPositionState.MissingThumbnail -> Matrix.IDENTITY_MATRIX
}
- private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
-
- private fun isSnapshotSplashState(task: Task): Boolean {
- val thumbnailPresent = task.thumbnail?.thumbnail != null
- val taskLocked = task.isLocked
-
- return thumbnailPresent && !taskLocked
- }
-
- private fun createSnapshotState(task: Task): Snapshot {
- val thumbnailData = task.thumbnail
- val bitmap = thumbnailData?.thumbnail!!
- 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)
-
private companion object {
const val MAX_SCRIM_ALPHA = 0.4f
const val TAG = "TaskThumbnailViewModel"
diff --git a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
index 1dab18a..e353160 100644
--- a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
+++ b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
@@ -21,7 +21,9 @@
import android.graphics.drawable.shapes.RoundRectShape
import android.util.AttributeSet
import android.widget.ImageButton
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
import com.android.launcher3.R
+import com.android.launcher3.util.MultiPropertyFactory
/**
* Button for supporting multiple desktop sessions. The button will be next to the first TaskView
@@ -30,6 +32,29 @@
class AddDesktopButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
ImageButton(context, attrs) {
+ private enum class TranslationX {
+ GRID,
+ OFFSET,
+ }
+
+ private val multiTranslationX =
+ MultiPropertyFactory(this, VIEW_TRANSLATE_X, TranslationX.entries.size) { a: Float, b: Float
+ ->
+ a + b
+ }
+
+ var gridTranslationX
+ get() = multiTranslationX[TranslationX.GRID.ordinal].value
+ set(value) {
+ multiTranslationX[TranslationX.GRID.ordinal].value = value
+ }
+
+ var offsetTranslationX
+ get() = multiTranslationX[TranslationX.OFFSET.ordinal].value
+ set(value) {
+ multiTranslationX[TranslationX.OFFSET.ordinal].value = value
+ }
+
override fun onFinishInflate() {
super.onFinishInflate()
diff --git a/quickstep/src/com/android/quickstep/views/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt
index 2e6c4bf..6da52d6 100644
--- a/quickstep/src/com/android/quickstep/views/IconView.kt
+++ b/quickstep/src/com/android/quickstep/views/IconView.kt
@@ -45,11 +45,11 @@
private var msdlPlayerWrapper: MSDLPlayerWrapper? = null
constructor(context: Context) : super(context) {
- msdlPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(context)
+ setUpHaptics()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
- msdlPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(context)
+ setUpHaptics()
}
constructor(
@@ -57,11 +57,15 @@
attrs: AttributeSet?,
defStyleAttr: Int,
) : super(context, attrs, defStyleAttr) {
- msdlPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(context)
+ setUpHaptics()
}
init {
multiValueAlpha.setUpdateVisibility(true)
+ }
+
+ private fun setUpHaptics() {
+ msdlPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(context)
// Haptics are handled by the MSDLPlayerWrapper
isHapticFeedbackEnabled = !Flags.msdlFeedback() || msdlPlayerWrapper == null
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index c0b026b..b93a2f0 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -3467,7 +3467,7 @@
// `mAddDesktopButton`, shift `mAddDesktopButton` to accommodate.
translationX += largeTaskWidthAndSpacing;
}
- mAddDesktopButton.setTranslationX(translationX);
+ mAddDesktopButton.setGridTranslationX(translationX);
}
final TaskView runningTask = getRunningTaskView();
@@ -4973,8 +4973,8 @@
} else if (child instanceof ClearAllButton) {
getPagedOrientationHandler().getPrimaryViewTranslate().set(child,
totalTranslationX);
- } else {
- // TODO(b/389209581): Handle the page offsets update of the 'mAddDesktopButton'.
+ } else if (child instanceof AddDesktopButton addDesktopButton) {
+ addDesktopButton.setOffsetTranslationX(totalTranslationX);
}
if (mEnableDrawingLiveTile && i == getRunningTaskIndex()) {
runActionOnRemoteHandles(
@@ -6157,7 +6157,7 @@
if (addDesktopButtonIndex != -1 && addDesktopButtonIndex < outPageScrolls.length) {
outPageScrolls[addDesktopButtonIndex] =
newPageScrolls[addDesktopButtonIndex] + Math.round(
- mAddDesktopButton.getTranslationX());
+ mAddDesktopButton.getGridTranslationX());
}
int lastTaskScroll = getLastTaskScroll(clearAllScroll, clearAllWidth);
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 7ac0946..6b5d8dd 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -28,6 +28,8 @@
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
@@ -163,4 +165,8 @@
digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) }
overlay.addChildForAccessibility(outChildren)
}
+
+ fun setState(state: TaskData?, liveTile: Boolean, hasHeader: Boolean) {
+ thumbnailView.setState(TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile, hasHeader))
+ }
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index e7a395f..741297d 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -70,6 +70,7 @@
import com.android.launcher3.util.TraceHelper
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.launcher3.util.ViewPool
+import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.launcher3.util.rects.set
import com.android.quickstep.FullscreenDrawParams
import com.android.quickstep.RecentsModel
@@ -77,6 +78,11 @@
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.TaskViewUtils
import com.android.quickstep.orientation.RecentsPagedOrientationHandler
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
+import com.android.quickstep.recents.di.inject
+import com.android.quickstep.recents.ui.viewmodel.TaskTileUiState
+import com.android.quickstep.recents.ui.viewmodel.TaskViewModel
import com.android.quickstep.util.ActiveGestureErrorDetector
import com.android.quickstep.util.ActiveGestureLog
import com.android.quickstep.util.BorderAnimator
@@ -88,6 +94,12 @@
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import com.android.systemui.shared.system.ActivityManagerWrapper
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
/** A task in the Recents view. */
open class TaskView
@@ -448,6 +460,11 @@
private val settledProgressDismiss =
settledProgressPropertyFactory.get(SETTLED_PROGRESS_INDEX_DISMISS)
+ private var viewModel: TaskViewModel? = null
+ private val dispatcherProvider: DispatcherProvider by RecentsDependencies.inject()
+ private val coroutineScope by lazy { CoroutineScope(SupervisorJob() + dispatcherProvider.main) }
+ private val coroutineJobs = mutableListOf<Job>()
+
/**
* Returns an animator of [settledProgressDismiss] that transition in with a built-in
* interpolator.
@@ -601,6 +618,8 @@
override fun onRecycle() {
resetPersistentViewTransforms()
+
+ viewModel = null
attachAlpha = 1f
splitAlpha = 1f
// Clear any references to the thumbnail (it will be re-read either from the cache or the
@@ -715,6 +734,44 @@
?.inflate()
}
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ if (enableRefactorTaskThumbnail()) {
+ // The TaskView lifecycle is starts the ViewModel during onBind, and cleans it in
+ // onRecycle. So it should be initialized at this point. TaskView Lifecycle:
+ // `bind` -> `onBind` -> onAttachedToWindow() -> onDetachFromWindow -> onRecycle
+ coroutineJobs +=
+ coroutineScope.launch {
+ viewModel!!.state.collectLatest(::updateTaskContainerState)
+ }
+ }
+ }
+
+ private fun updateTaskContainerState(state: TaskTileUiState) {
+ val mapOfTasks = state.tasks.associateBy { it.taskId }
+ taskContainers.forEach { container ->
+ container.setState(
+ state = mapOfTasks[container.task.key.id],
+ liveTile = state.isLiveTile,
+ hasHeader = type == TaskViewType.DESKTOP,
+ )
+ }
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ if (enableRefactorTaskThumbnail()) {
+ // The jobs are being cancelled in the background thread. So we make a copy of the list
+ // to prevent cleaning a new job that might be added to this list during onAttach
+ // or another moment in the lifecycle.
+ val coroutineJobsToCancel = coroutineJobs.toList()
+ coroutineJobs.clear()
+ coroutineScope.launch(dispatcherProvider.background) {
+ coroutineJobsToCancel.forEach { it.cancel("TaskView detaching from window") }
+ }
+ }
+ }
+
/** Updates this task view to the given {@param task}. */
open fun bind(
task: Task,
@@ -738,6 +795,17 @@
}
open fun onBind(orientedState: RecentsOrientedState) {
+ if (enableRefactorTaskThumbnail()) {
+ viewModel =
+ TaskViewModel(
+ taskViewType = type,
+ recentsViewData = RecentsDependencies.get(),
+ getTaskUseCase = RecentsDependencies.get(),
+ dispatcherProvider = RecentsDependencies.get(),
+ )
+ .apply { bind(*taskIds) }
+ }
+
taskContainers.forEach {
it.bind()
if (enableRefactorTaskThumbnail()) {
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 47d2bfc..e033e7b 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
@@ -17,14 +17,12 @@
package com.android.quickstep.task.thumbnail
import android.graphics.Matrix
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import kotlinx.coroutines.flow.MutableStateFlow
class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
override val dimProgress = MutableStateFlow(0f)
override val splashAlpha = MutableStateFlow(0f)
- override val uiState = MutableStateFlow<TaskThumbnailUiState>(Uninitialized)
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 a76f83c..3b28afd 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
@@ -60,36 +60,27 @@
screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
activity.actionBar?.hide()
val taskThumbnailView = createTaskThumbnailView(activity)
- taskThumbnailViewModel.uiState.value = BackgroundOnly(Color.YELLOW)
- taskThumbnailViewModel.uiState.value = Uninitialized
+ taskThumbnailView.setState(Uninitialized)
taskThumbnailView
}
}
@Test
fun taskThumbnailView_recyclesToUninitialized() {
- screenshotRule.screenshotTest(
- "taskThumbnailView_uninitialized",
- viewProvider = { activity ->
- activity.actionBar?.hide()
- val taskThumbnailView = createTaskThumbnailView(activity)
- taskThumbnailViewModel.uiState.value = BackgroundOnly(Color.YELLOW)
- taskThumbnailView
- },
- checkView = { _, taskThumbnailView ->
- // Call onRecycle() after View is attached (end of block above)
- (taskThumbnailView as TaskThumbnailView).onRecycle()
- false
- },
- )
+ screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
+ activity.actionBar?.hide()
+ val taskThumbnailView = createTaskThumbnailView(activity)
+ taskThumbnailView.setState(BackgroundOnly(Color.YELLOW))
+ taskThumbnailView.onRecycle()
+ taskThumbnailView
+ }
}
@Test
fun taskThumbnailView_backgroundOnly() {
screenshotRule.screenshotTest("taskThumbnailView_backgroundOnly") { activity ->
activity.actionBar?.hide()
- taskThumbnailViewModel.uiState.value = BackgroundOnly(Color.YELLOW)
- createTaskThumbnailView(activity)
+ createTaskThumbnailView(activity).apply { setState(BackgroundOnly(Color.YELLOW)) }
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt
new file mode 100644
index 0000000..124045f
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt
@@ -0,0 +1,231 @@
+/*
+ * 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.ui.mapper
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.ShapeDrawable
+import android.platform.test.annotations.EnableFlags
+import android.view.Surface
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Flags
+import com.android.quickstep.recents.ui.viewmodel.TaskData
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TaskUiStateMapperTest {
+
+ @Test
+ fun taskData_isNull_returns_Uninitialized() {
+ val result =
+ TaskUiStateMapper.toTaskThumbnailUiState(
+ taskData = null,
+ isLiveTile = false,
+ hasHeader = false,
+ )
+ assertThat(result).isEqualTo(TaskThumbnailUiState.Uninitialized)
+ }
+
+ @Test
+ fun taskData_isLiveTile_returns_LiveTile() {
+ val inputs =
+ listOf(TASK_DATA, TASK_DATA.copy(thumbnailData = null), TASK_DATA.copy(isLocked = true))
+ inputs.forEach { input ->
+ val result =
+ TaskUiStateMapper.toTaskThumbnailUiState(
+ taskData = input,
+ isLiveTile = true,
+ hasHeader = false,
+ )
+ assertThat(result).isEqualTo(LiveTile.WithoutHeader)
+ }
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
+ @Test
+ fun taskData_isLiveTileWithHeader_returns_LiveTileWithHeader() {
+ val inputs =
+ listOf(
+ TASK_DATA,
+ TASK_DATA.copy(thumbnailData = null),
+ TASK_DATA.copy(isLocked = true),
+ TASK_DATA.copy(title = null),
+ )
+ val expected =
+ LiveTile.WithHeader(header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION))
+ inputs.forEach { taskData ->
+ val result =
+ TaskUiStateMapper.toTaskThumbnailUiState(
+ taskData = taskData,
+ isLiveTile = true,
+ hasHeader = true,
+ )
+ assertThat(result).isEqualTo(expected)
+ }
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
+ @Test
+ fun taskData_isLiveTileWithHeader_missingHeaderData_returns_LiveTileWithoutHeader() {
+ val inputs =
+ listOf(
+ TASK_DATA.copy(icon = null),
+ TASK_DATA.copy(titleDescription = null),
+ TASK_DATA.copy(icon = null, titleDescription = null),
+ )
+
+ inputs.forEach { taskData ->
+ val result =
+ TaskUiStateMapper.toTaskThumbnailUiState(
+ taskData = taskData,
+ isLiveTile = true,
+ hasHeader = true,
+ )
+ assertThat(result).isEqualTo(LiveTile.WithoutHeader)
+ }
+ }
+
+ @Test
+ fun taskData_isStaticTile_returns_SnapshotSplash() {
+ val result =
+ TaskUiStateMapper.toTaskThumbnailUiState(
+ taskData = TASK_DATA,
+ isLiveTile = false,
+ hasHeader = false,
+ )
+
+ val expected =
+ TaskThumbnailUiState.SnapshotSplash(
+ snapshot =
+ Snapshot.WithoutHeader(
+ backgroundColor = TASK_BACKGROUND_COLOR,
+ bitmap = TASK_THUMBNAIL,
+ thumbnailRotation = Surface.ROTATION_0,
+ ),
+ splash = TASK_ICON,
+ )
+
+ assertThat(result).isEqualTo(expected)
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
+ @Test
+ fun taskData_isStaticTile_withHeader_returns_SnapshotSplashWithHeader() {
+ val inputs = listOf(TASK_DATA, TASK_DATA.copy(title = null))
+ val expected =
+ TaskThumbnailUiState.SnapshotSplash(
+ snapshot =
+ Snapshot.WithHeader(
+ backgroundColor = TASK_BACKGROUND_COLOR,
+ bitmap = TASK_THUMBNAIL,
+ thumbnailRotation = Surface.ROTATION_0,
+ header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION),
+ ),
+ splash = TASK_ICON,
+ )
+ inputs.forEach { taskData ->
+ val result =
+ TaskUiStateMapper.toTaskThumbnailUiState(
+ taskData = taskData,
+ isLiveTile = false,
+ hasHeader = true,
+ )
+ assertThat(result).isEqualTo(expected)
+ }
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
+ @Test
+ fun taskData_isStaticTile_missingHeaderData_returns_SnapshotSplashWithoutHeader() {
+ val inputs =
+ listOf(
+ TASK_DATA.copy(titleDescription = null, icon = null),
+ TASK_DATA.copy(titleDescription = null),
+ TASK_DATA.copy(icon = null),
+ )
+ val expected =
+ Snapshot.WithoutHeader(
+ backgroundColor = TASK_BACKGROUND_COLOR,
+ thumbnailRotation = Surface.ROTATION_0,
+ bitmap = TASK_THUMBNAIL,
+ )
+ inputs.forEach { taskData ->
+ val result =
+ TaskUiStateMapper.toTaskThumbnailUiState(
+ taskData = taskData,
+ isLiveTile = false,
+ hasHeader = true,
+ )
+
+ assertThat(result).isInstanceOf(TaskThumbnailUiState.SnapshotSplash::class.java)
+ result as TaskThumbnailUiState.SnapshotSplash
+ assertThat(result.snapshot).isEqualTo(expected)
+ }
+ }
+
+ @Test
+ fun taskData_thumbnailIsNull_returns_BackgroundOnly() {
+ val result =
+ TaskUiStateMapper.toTaskThumbnailUiState(
+ taskData = TASK_DATA.copy(thumbnailData = null),
+ isLiveTile = false,
+ hasHeader = false,
+ )
+
+ val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR)
+ assertThat(result).isEqualTo(expected)
+ }
+
+ @Test
+ fun taskData_isLocked_returns_BackgroundOnly() {
+ val result =
+ TaskUiStateMapper.toTaskThumbnailUiState(
+ taskData = TASK_DATA.copy(isLocked = true),
+ isLiveTile = false,
+ hasHeader = false,
+ )
+
+ val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR)
+ assertThat(result).isEqualTo(expected)
+ }
+
+ private companion object {
+ const val TASK_TITLE_DESCRIPTION = "Title Description 1"
+ val TASK_ICON = ShapeDrawable()
+ val TASK_THUMBNAIL = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+ val TASK_THUMBNAIL_DATA =
+ ThumbnailData(thumbnail = TASK_THUMBNAIL, rotation = Surface.ROTATION_0)
+ val TASK_BACKGROUND_COLOR = Color.rgb(1, 2, 3)
+ val TASK_DATA =
+ TaskData.Data(
+ 1,
+ title = "Task 1",
+ titleDescription = TASK_TITLE_DESCRIPTION,
+ icon = TASK_ICON,
+ thumbnailData = TASK_THUMBNAIL_DATA,
+ backgroundColor = TASK_BACKGROUND_COLOR,
+ isLocked = false,
+ )
+ }
+}
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 54a27e9..7a4b5f2 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
@@ -23,6 +23,7 @@
import com.android.quickstep.recents.domain.model.TaskModel
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -45,15 +46,17 @@
private val recentsViewData = RecentsViewData()
private val getTaskUseCase = mock<GetTaskUseCase>()
- private val sut =
- TaskViewModel(
- recentsViewData = recentsViewData,
- getTaskUseCase = getTaskUseCase,
- dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
- )
+ private lateinit var sut: TaskViewModel
@Before
fun setUp() {
+ sut =
+ TaskViewModel(
+ taskViewType = TaskViewType.SINGLE,
+ recentsViewData = recentsViewData,
+ getTaskUseCase = getTaskUseCase,
+ dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
+ )
whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) })
whenever(getTaskUseCase.invoke(TASK_MODEL_2.id)).thenReturn(flow { emit(TASK_MODEL_2) })
whenever(getTaskUseCase.invoke(TASK_MODEL_3.id)).thenReturn(flow { emit(TASK_MODEL_3) })
@@ -65,11 +68,39 @@
fun singleTaskRetrieved_when_validTaskId() =
testScope.runTest {
sut.bind(TASK_MODEL_1.id)
- val expectedResult = TaskTileUiState(listOf(TASK_MODEL_1.toUiState()), false)
+ val expectedResult =
+ TaskTileUiState(
+ tasks = listOf(TASK_MODEL_1.toUiState()),
+ isLiveTile = false,
+ hasHeader = false,
+ )
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@Test
+ fun hasHeader_when_taskViewTypeIsDesktop() =
+ testScope.runTest {
+ val expectedResults =
+ mapOf(
+ TaskViewType.SINGLE to false,
+ TaskViewType.GROUPED to false,
+ TaskViewType.DESKTOP to true,
+ )
+
+ expectedResults.forEach { (type, expectedResult) ->
+ sut =
+ TaskViewModel(
+ taskViewType = type,
+ recentsViewData = recentsViewData,
+ getTaskUseCase = getTaskUseCase,
+ dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
+ )
+ sut.bind(TASK_MODEL_1.id)
+ assertThat(sut.state.first().hasHeader).isEqualTo(expectedResult)
+ }
+ }
+
+ @Test
fun multipleTasksRetrieved_when_validTaskIds() =
testScope.runTest {
sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id, INVALID_TASK_ID)
@@ -83,6 +114,7 @@
TaskData.NoData(INVALID_TASK_ID),
),
isLiveTile = false,
+ hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -103,6 +135,7 @@
TASK_MODEL_3.toUiState(),
),
isLiveTile = true,
+ hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -123,6 +156,7 @@
TASK_MODEL_3.toUiState(),
),
isLiveTile = false,
+ hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -142,6 +176,7 @@
TASK_MODEL_3.toUiState(),
),
isLiveTile = false,
+ hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -157,6 +192,7 @@
TaskTileUiState(
tasks = listOf(TASK_MODEL_1.toUiState(), TASK_MODEL_2.toUiState()),
isLiveTile = false,
+ hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -166,7 +202,11 @@
testScope.runTest {
sut.bind(INVALID_TASK_ID)
val expectedResult =
- TaskTileUiState(listOf(TaskData.NoData(INVALID_TASK_ID)), isLiveTile = false)
+ TaskTileUiState(
+ listOf(TaskData.NoData(INVALID_TASK_ID)),
+ isLiveTile = false,
+ hasHeader = false,
+ )
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -174,6 +214,7 @@
TaskData.Data(
taskId = id,
title = title,
+ titleDescription = titleDescription,
icon = icon!!,
thumbnailData = thumbnail,
backgroundColor = backgroundColor,
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 a956c9c..22636b0 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,37 +16,16 @@
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
import com.android.quickstep.recents.viewmodel.RecentsViewData
-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
-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.StandardTestDispatcher
@@ -69,8 +48,6 @@
private val recentsViewData = RecentsViewData()
private val taskContainerData = TaskContainerData()
private val dispatcherProvider = TestDispatcherProvider(dispatcher)
- private val tasksRepository = FakeTasksRepository()
- private val deviceProfileRepository = FakeRecentsDeviceProfileRepository()
private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
private val splashAlphaUseCase: SplashAlphaUseCase = mock()
@@ -79,208 +56,11 @@
recentsViewData,
taskContainerData,
dispatcherProvider,
- tasksRepository,
- deviceProfileRepository,
mGetThumbnailPositionUseCase,
splashAlphaUseCase,
)
}
- 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() =
- testScope.runTest { assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized) }
-
- @Test
- fun bindRunningTask_thenStateIs_LiveTile() =
- testScope.runTest {
- val taskId = 1
- tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(setOf(taskId))
- recentsViewData.runningTaskIds.value = setOf(taskId)
- systemUnderTest.bind(taskId)
-
- assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile.WithoutHeader)
- }
-
- @Test
- fun bindRunningTaskShouldShowScreenshot_thenStateIs_SnapshotSplash() =
- testScope.runTest {
- val taskId = 1
- val expectedThumbnailData = createThumbnailData()
- tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
- val expectedIconData = mock<Drawable>()
- tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
- tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(setOf(taskId))
- recentsViewData.runningTaskIds.value = setOf(taskId)
- recentsViewData.runningTaskShowScreenshot.value = true
- systemUnderTest.bind(taskId)
-
- assertThat(systemUnderTest.uiState.first())
- .isEqualTo(
- SnapshotSplash(
- Snapshot.WithoutHeader(
- backgroundColor = Color.rgb(1, 1, 1),
- bitmap = expectedThumbnailData.thumbnail!!,
- thumbnailRotation = Surface.ROTATION_0,
- ),
- expectedIconData,
- )
- )
- }
-
- @Test
- fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() =
- testScope.runTest {
- val runningTaskId = 1
- val stoppedTaskId = 2
- tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(setOf(runningTaskId, stoppedTaskId))
- recentsViewData.runningTaskIds.value = setOf(runningTaskId)
- systemUnderTest.bind(runningTaskId)
- assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile.WithoutHeader)
-
- systemUnderTest.bind(stoppedTaskId)
- assertThat(systemUnderTest.uiState.first())
- .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
- }
-
- @Test
- fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() =
- testScope.runTest {
- val stoppedTaskId = 2
- tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(setOf(stoppedTaskId))
-
- systemUnderTest.bind(stoppedTaskId)
- assertThat(systemUnderTest.uiState.first())
- .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
- }
-
- @Test
- fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() =
- testScope.runTest {
- val taskId = 2
- tasksRepository.seedThumbnailData(mapOf(taskId to createThumbnailData()))
- tasks[taskId].isLocked = true
- tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(setOf(taskId))
-
- systemUnderTest.bind(taskId)
- assertThat(systemUnderTest.uiState.first())
- .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
- }
-
- @Test
- fun bindStoppedTaskWithThumbnail_thenStateIs_SnapshotSplash_withAlphaRemoved() =
- testScope.runTest {
- val taskId = 2
- val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
- tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
- val expectedIconData = mock<Drawable>()
- tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
- tasksRepository.seedTasks(tasks)
- tasksRepository.setVisibleTasks(setOf(taskId))
-
- systemUnderTest.bind(taskId)
- assertThat(systemUnderTest.uiState.first())
- .isEqualTo(
- SnapshotSplash(
- Snapshot.WithoutHeader(
- backgroundColor = Color.rgb(2, 2, 2),
- bitmap = expectedThumbnailData.thumbnail!!,
- thumbnailRotation = Surface.ROTATION_270,
- ),
- expectedIconData,
- )
- )
- }
-
- @Test
- fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshotSplash() =
- testScope.runTest {
- val taskId = 2
- val expectedThumbnailData = createThumbnailData()
- tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
- val expectedIconData = mock<Drawable>()
- tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
- tasksRepository.seedTasks(tasks)
-
- systemUnderTest.bind(taskId)
- assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
-
- tasksRepository.setVisibleTasks(setOf(taskId))
- assertThat(systemUnderTest.uiState.first())
- .isEqualTo(
- SnapshotSplash(
- Snapshot.WithoutHeader(
- backgroundColor = Color.rgb(2, 2, 2),
- bitmap = expectedThumbnailData.thumbnail!!,
- thumbnailRotation = Surface.ROTATION_0,
- ),
- expectedIconData,
- )
- )
- }
-
- @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<Drawable>()
- 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<Drawable>()
- 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 {
@@ -337,51 +117,7 @@
assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0f)
}
- private fun createTaskWithId(taskId: Int) =
- Task(
- Task.TaskKey(
- taskId,
- WINDOWING_MODE_FULLSCREEN,
- Intent(),
- ComponentName("", ""),
- 0,
- 2000,
- )
- )
- .apply {
- colorBackground = Color.argb(taskId, taskId, taskId, taskId)
- titleDescription = "Task $taskId"
- icon = mock<Drawable>()
- }
-
- 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<Drawable>()
- }
-
- private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
- val bitmap = mock<Bitmap>()
- whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
- whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
-
- return ThumbnailData(thumbnail = bitmap, rotation = rotation)
- }
-
- companion object {
- const val THUMBNAIL_WIDTH = 100
- const val THUMBNAIL_HEIGHT = 200
+ private companion object {
const val CANVAS_WIDTH = 300
const val CANVAS_HEIGHT = 600
val MATRIX =