diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig index 8682e5d6b4..9147e4c54f 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -284,10 +284,3 @@ flag { description: "Enables folders in all apps" bug: "341582436" } - -flag { - name: "enable_tiny_taskbar" - namespace: "launcher" - description: "Enables Taskbar on phones" - bug: "341784466" -} diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 2021a0b96a..08d36d8d87 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -358,6 +358,9 @@ 4dp 14dp 2dp + 2dp + 12dp + 2dp 12dp diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt index 3649c4ebc5..d4bef28bf8 100644 --- a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt @@ -47,6 +47,7 @@ class DesktopTaskbarRunningAppsController( private var apps: Array? = null private var allRunningDesktopAppInfos: List? = null + private var allMinimizedDesktopAppInfos: List? = null private val desktopVisibilityController: DesktopVisibilityController? get() = desktopVisibilityControllerProvider() @@ -95,6 +96,13 @@ class DesktopTaskbarRunningAppsController( return allRunningDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() ?: emptySet() } + override fun getMinimizedApps(): Set { + if (!isInDesktopMode) { + return emptySet() + } + return allMinimizedDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() ?: emptySet() + } + @VisibleForTesting public override fun updateRunningApps() { if (!isInDesktopMode) { @@ -102,10 +110,34 @@ class DesktopTaskbarRunningAppsController( mControllers.taskbarViewController.commitRunningAppsToUI() return } - allRunningDesktopAppInfos = getRunningDesktopAppInfos() + val runningTasks = getDesktopRunningTasks() + val runningAppInfo = getAppInfosFromRunningTasks(runningTasks) + allRunningDesktopAppInfos = runningAppInfo + updateMinimizedApps(runningTasks, runningAppInfo) mControllers.taskbarViewController.commitRunningAppsToUI() } + private fun updateMinimizedApps( + runningTasks: List, + runningAppInfo: List, + ) { + val allRunningAppTasks = + runningAppInfo + .mapNotNull { appInfo -> appInfo.targetPackage?.let { appInfo to it } } + .associate { (appInfo, targetPackage) -> + appInfo to + runningTasks + .filter { it.realActivity?.packageName == targetPackage } + .map { it.taskId } + } + val minimizedTaskIds = runningTasks.associate { it.taskId to !it.isVisible } + allMinimizedDesktopAppInfos = + allRunningAppTasks + .filterValues { taskIds -> taskIds.all { minimizedTaskIds[it] ?: false } } + .keys + .toList() + } + private fun getRunningDesktopAppInfosExceptHotseatApps( allRunningDesktopAppInfos: List, hotseatItems: List @@ -116,15 +148,10 @@ class DesktopTaskbarRunningAppsController( .map { WorkspaceItemInfo(it) } } - private fun getRunningDesktopAppInfos(): List { - return getAppInfosFromRunningTasks( - recentsModel.runningTasks - .filter { taskInfo: RunningTaskInfo -> - taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM - } - .toList() - ) - } + private fun getDesktopRunningTasks(): List = + recentsModel.runningTasks.filter { taskInfo: RunningTaskInfo -> + taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM + } // TODO(b/335398876) fetch app icons from Tasks instead of AppInfos private fun getAppInfosFromRunningTasks(tasks: List): List { @@ -138,9 +165,10 @@ class DesktopTaskbarRunningAppsController( .filterNotNull() } - private fun SparseArray.toList(): List { - return valueIterator().asSequence().toList() - } + private fun getAppInfosFromRunningTask(task: RunningTaskInfo): AppInfo? = + apps?.firstOrNull { it.targetPackage == task.realActivity?.packageName } + + private fun SparseArray.toList(): List = valueIterator().asSequence().toList() companion object { private const val TAG = "TabletDesktopTaskbarRunningAppsController" diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java index b213203cb1..358d703b75 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java @@ -148,7 +148,7 @@ public final class KeyboardQuickSwitchController implements }); } - private void processLoadedTasks(ArrayList tasks) { + private void processLoadedTasks(List tasks) { // Only store MAX_TASK tasks, from most to least recent Collections.reverse(tasks); mTasks = tasks.stream() @@ -157,7 +157,7 @@ public final class KeyboardQuickSwitchController implements mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS); } - private void processLoadedTasksOnDesktop(ArrayList tasks) { + private void processLoadedTasksOnDesktop(List tasks) { // Find the single desktop task that contains a grouping of desktop tasks DesktopTask desktopTask = findDesktopTask(tasks); @@ -173,7 +173,7 @@ public final class KeyboardQuickSwitchController implements } @Nullable - private DesktopTask findDesktopTask(ArrayList tasks) { + private DesktopTask findDesktopTask(List tasks) { return (DesktopTask) tasks.stream() .filter(t -> t instanceof DesktopTask) .findFirst() diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index 1571ac0ea5..0de0550016 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.systemui.shared.system.QuickStepContract.SYSUI_STATE_N import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING; import static com.android.window.flags.Flags.enableDesktopWindowingMode; import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps; +import static com.android.wm.shell.Flags.enableTinyTaskbar; import android.animation.AnimatorSet; import android.animation.ValueAnimator; @@ -416,7 +417,9 @@ public class TaskbarActivityContext extends BaseTaskbarContext { * single window for taskbar and navbar. */ public boolean isPhoneMode() { - return ENABLE_TASKBAR_NAVBAR_UNIFICATION && mDeviceProfile.isPhone; + return ENABLE_TASKBAR_NAVBAR_UNIFICATION + && mDeviceProfile.isPhone + && !mDeviceProfile.isTaskbarPresent; } /** @@ -433,6 +436,11 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return isPhoneMode() && !isThreeButtonNav(); } + /** Returns {@code true} iff a tiny version of taskbar is shown on phone. */ + public boolean isTinyTaskbar() { + return enableTinyTaskbar() && mDeviceProfile.isPhone && mDeviceProfile.isTaskbarPresent; + } + /** * Returns if software keyboard is docked or input toolbar is placed at the taskbar area */ @@ -981,7 +989,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { public int getDefaultTaskbarWindowSize() { Resources resources = getResources(); - if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && mDeviceProfile.isPhone) { + if (isPhoneMode()) { return isThreeButtonNav() ? resources.getDimensionPixelSize(R.dimen.taskbar_phone_size) : resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt index d43055d56a..5cbd5c95f0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt @@ -81,7 +81,11 @@ open class TaskbarEduTooltipController(context: Context) : protected val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context) open val shouldShowSearchEdu = false private val isTooltipEnabled: Boolean - get() = !Utilities.isRunningInTestHarness() && !activityContext.isPhoneMode + get() { + return !Utilities.isRunningInTestHarness() && + !activityContext.isPhoneMode && + !activityContext.isTinyTaskbar + } private val isOpen: Boolean get() = tooltip?.isOpen ?: false val isBeforeTooltipFeaturesStep: Boolean diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt index 4a8ed87d19..e1ddb6a951 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt @@ -51,6 +51,7 @@ import com.android.launcher3.anim.AlphaUpdateListener import com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION import com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController +import com.android.launcher3.testing.shared.ResourceUtils import com.android.launcher3.util.DisplayController import java.io.PrintWriter import kotlin.jvm.optionals.getOrNull @@ -231,8 +232,24 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas val contentHeight = controllers.taskbarStashController.contentHeightToReportToApps val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps val res = context.resources - if (provider.type == navigationBars() || provider.type == mandatorySystemGestures()) { + if (provider.type == navigationBars()) { provider.insetsSize = getInsetsForGravityWithCutout(contentHeight, gravity, endRotation) + } else if (provider.type == mandatorySystemGestures()) { + if (context.isThreeButtonNav) { + provider.insetsSize = Insets.of(0, 0, 0, 0) + } else { + val gestureHeight = + ResourceUtils.getNavbarSize( + ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, + context.resources) + val isPinnedTaskbar = context.deviceProfile.isTaskbarPresent + && !context.deviceProfile.isTransientTaskbar + val mandatoryGestureHeight = + if (isPinnedTaskbar) contentHeight + else gestureHeight + provider.insetsSize = getInsetsForGravityWithCutout(mandatoryGestureHeight, gravity, + endRotation) + } } else if (provider.type == tappableElement()) { provider.insetsSize = getInsetsForGravity(tappableHeight, gravity) } else if (provider.type == systemGestures() && provider.index == INDEX_LEFT) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java index e8dc177d0e..ec2cee2bd9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java @@ -519,7 +519,7 @@ public class TaskbarManager { } } - private static boolean isTaskbarEnabled(DeviceProfile deviceProfile) { + private boolean isTaskbarEnabled(DeviceProfile deviceProfile) { return ENABLE_TASKBAR_NAVBAR_UNIFICATION || deviceProfile.isTaskbarPresent; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java index 9f24d38664..35e1c7baa9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java @@ -68,7 +68,7 @@ public class TaskbarModelCallbacks implements // Used to defer any UI updates during the SUW unstash animation. private boolean mDeferUpdatesForSUW; private Runnable mDeferredUpdates; - private DesktopVisibilityController.DesktopVisibilityListener mDesktopVisibilityListener = + private final DesktopVisibilityController.DesktopVisibilityListener mDesktopVisibilityListener = visible -> updateRunningApps(); public TaskbarModelCallbacks( @@ -235,20 +235,23 @@ public class TaskbarModelCallbacks implements hotseatItemInfos = mControllers.taskbarRecentAppsController .updateHotseatItemInfos(hotseatItemInfos); Set runningPackages = mControllers.taskbarRecentAppsController.getRunningApps(); + Set minimizedPackages = mControllers.taskbarRecentAppsController.getMinimizedApps(); if (mDeferUpdatesForSUW) { ItemInfo[] finalHotseatItemInfos = hotseatItemInfos; mDeferredUpdates = () -> - commitHotseatItemUpdates(finalHotseatItemInfos, runningPackages); + commitHotseatItemUpdates(finalHotseatItemInfos, runningPackages, + minimizedPackages); } else { - commitHotseatItemUpdates(hotseatItemInfos, runningPackages); + commitHotseatItemUpdates(hotseatItemInfos, runningPackages, minimizedPackages); } } - private void commitHotseatItemUpdates( - ItemInfo[] hotseatItemInfos, Set runningPackages) { + private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, Set runningPackages, + Set minimizedPackages) { mContainer.updateHotseatItems(hotseatItemInfos); - mControllers.taskbarViewController.updateIconViewsRunningStates(runningPackages); + mControllers.taskbarViewController.updateIconViewsRunningStates(runningPackages, + minimizedPackages); } /** diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java index a29c74bf06..606ba5b633 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java @@ -69,4 +69,9 @@ public class TaskbarRecentAppsController { public Set getRunningApps() { return emptySet(); } + + /** Returns the set of apps whose tasks are all minimized. */ + public Set getMinimizedApps() { + return emptySet(); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index 93814b7008..23495adcf7 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -510,14 +510,30 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar } /** Updates which icons are marked as running given the Set of currently running packages. */ - public void updateIconViewsRunningStates(Set runningPackages) { + public void updateIconViewsRunningStates(Set runningPackages, + Set minimizedPackages) { for (View iconView : getIconViews()) { if (iconView instanceof BubbleTextView btv) { - btv.updateRunningState(runningPackages.contains(btv.getTargetPackageName())); + btv.updateRunningState( + getRunningAppState(btv.getTargetPackageName(), runningPackages, + minimizedPackages)); } } } + private BubbleTextView.RunningAppState getRunningAppState( + String packageName, + Set runningPackages, + Set minimizedPackages) { + if (minimizedPackages.contains(packageName)) { + return BubbleTextView.RunningAppState.MINIMIZED; + } + if (runningPackages.contains(packageName)) { + return BubbleTextView.RunningAppState.RUNNING; + } + return BubbleTextView.RunningAppState.NOT_RUNNING; + } + /** * Defers any updates to the UI for the setup wizard animation. */ diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java index 711882c7b6..37b4dcabb8 100644 --- a/quickstep/src/com/android/quickstep/RecentTasksList.java +++ b/quickstep/src/com/android/quickstep/RecentTasksList.java @@ -31,6 +31,7 @@ import android.os.Process; import android.os.RemoteException; import android.util.SparseBooleanArray; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.util.LooperExecutor; @@ -44,6 +45,7 @@ import com.android.wm.shell.util.GroupedRecentTaskInfo; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -137,7 +139,7 @@ public class RecentTasksList { * @return The change id of the current task list */ public synchronized int getTasks(boolean loadKeysOnly, - Consumer> callback, Predicate filter) { + @Nullable Consumer> callback, Predicate filter) { final int requestLoadId = mChangeId; if (mResultsUi.isValidForRequest(requestLoadId, loadKeysOnly)) { // The list is up to date, send the callback on the next frame, diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java index 89351aa2d2..98c1eb409c 100644 --- a/quickstep/src/com/android/quickstep/RecentsModel.java +++ b/quickstep/src/com/android/quickstep/RecentsModel.java @@ -33,6 +33,7 @@ import android.os.Build; import android.os.Process; import android.os.UserHandle; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.icons.IconProvider; @@ -40,6 +41,7 @@ import com.android.launcher3.icons.IconProvider.IconChangeListener; import com.android.launcher3.util.Executors.SimpleThreadFactory; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.SafeCloseable; +import com.android.quickstep.recents.data.RecentTasksDataSource; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.TaskVisualsChangeListener; import com.android.systemui.shared.recents.model.Task; @@ -60,8 +62,8 @@ import java.util.function.Predicate; * Singleton class to load and manage recents model. */ @TargetApi(Build.VERSION_CODES.O) -public class RecentsModel implements IconChangeListener, TaskStackChangeListener, - TaskVisualsChangeListener, SafeCloseable { +public class RecentsModel implements RecentTasksDataSource, IconChangeListener, + TaskStackChangeListener, TaskVisualsChangeListener, SafeCloseable { // We do not need any synchronization for this variable as its only written on UI thread. public static final MainThreadInitializedObject INSTANCE = @@ -141,7 +143,8 @@ public class RecentsModel implements IconChangeListener, TaskStackChangeListener * always called on the UI thread. * @return the request id associated with this call. */ - public int getTasks(Consumer> callback) { + @Override + public int getTasks(@Nullable Consumer> callback) { return mTaskList.getTasks(false /* loadKeysOnly */, callback, RecentsFilterState.DEFAULT_FILTER); } @@ -155,7 +158,7 @@ public class RecentsModel implements IconChangeListener, TaskStackChangeListener * callback. * @return the request id associated with this call. */ - public int getTasks(Consumer> callback, Predicate filter) { + public int getTasks(@Nullable Consumer> callback, Predicate filter) { return mTaskList.getTasks(false /* loadKeysOnly */, callback, filter); } diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java index 7ebb767f01..38e927f95f 100644 --- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java +++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java @@ -21,11 +21,13 @@ import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import android.content.Context; import android.content.res.Resources; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.launcher3.R; import com.android.launcher3.util.CancellableTask; import com.android.launcher3.util.Preconditions; +import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource; import com.android.quickstep.util.TaskKeyByLastActiveTimeCache; import com.android.quickstep.util.TaskKeyCache; import com.android.quickstep.util.TaskKeyLruCache; @@ -38,7 +40,7 @@ import java.util.ArrayList; import java.util.concurrent.Executor; import java.util.function.Consumer; -public class TaskThumbnailCache { +public class TaskThumbnailCache implements TaskThumbnailDataSource { private final Executor mBgExecutor; private final TaskKeyCache mCache; @@ -148,8 +150,9 @@ public class TaskThumbnailCache { * @param callback The callback to receive the task after its data has been populated. * @return A cancelable handle to the request */ + @Override public CancellableTask updateThumbnailInBackground( - Task task, Consumer callback) { + Task task, @NonNull Consumer callback) { Preconditions.assertUIThread(); boolean lowResolution = !mHighResLoadingState.isEnabled(); diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java index 096ed2c0cb..485d6c4391 100644 --- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java +++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java @@ -54,6 +54,7 @@ import com.android.systemui.shared.recents.model.Task; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; public class FallbackRecentsView extends RecentsView implements StateListener { @@ -179,7 +180,7 @@ public class FallbackRecentsView extends RecentsView taskGroups) { + protected void applyLoadPlan(List taskGroups) { // When quick-switching on 3p-launcher, we add a "stub" tile corresponding to Launcher // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to // track the index of the next task appropriately, as if we are switching on any other app. diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksDataSource.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksDataSource.kt new file mode 100644 index 0000000000..6719099f7b --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksDataSource.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +import com.android.quickstep.util.GroupTask +import java.util.function.Consumer + +interface RecentTasksDataSource { + fun getTasks(callback: Consumer>?): Int +} diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt new file mode 100644 index 0000000000..ad8ae207d1 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +import com.android.quickstep.TaskIconCache +import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource +import com.android.quickstep.util.GroupTask +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import kotlin.coroutines.resume +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.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.suspendCancellableCoroutine + +@OptIn(ExperimentalCoroutinesApi::class) +class TasksRepository( + private val recentsModel: RecentTasksDataSource, + private val taskThumbnailDataSource: TaskThumbnailDataSource, + private val taskIconCache: TaskIconCache, +) { + private val groupedTaskData = MutableStateFlow(emptyList()) + private val _taskData = + groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } } + private val visibleTaskIds = MutableStateFlow(emptySet()) + + private val taskData: Flow> = + combine(_taskData, getThumbnailQueryResults()) { tasks, results -> + tasks.forEach { task -> + // Add retrieved thumbnails + remove unnecessary thumbnails + task.thumbnail = results[task.key.id] + } + tasks + } + + fun getAllTaskData(forceRefresh: Boolean = false): Flow> { + if (forceRefresh) { + recentsModel.getTasks { groupedTaskData.value = it } + } + return taskData + } + + fun getTaskDataById(taskId: Int): Flow = + taskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } } + + fun setVisibleTasks(visibleTaskIdList: List) { + this.visibleTaskIds.value = visibleTaskIdList.toSet() + } + + /** Flow wrapper for [TaskThumbnailDataSource.updateThumbnailInBackground] api */ + private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = + flow { + emit(task.key.id to task.thumbnail) + val thumbnailDataResult: ThumbnailData? = + suspendCancellableCoroutine { continuation -> + val cancellableTask = + taskThumbnailDataSource.updateThumbnailInBackground(task) { + continuation.resume(it) + } + continuation.invokeOnCancellation { cancellableTask?.cancel() } + } + emit(task.key.id to thumbnailDataResult) + } + .distinctUntilChanged() + + /** + * This is a Flow that makes a query for thumbnail data to the [taskThumbnailDataSource] for + * each visible task. It then collects the responses and returns them in a Map as soon as they + * are available. + */ + private fun getThumbnailQueryResults(): Flow> { + val visibleTasks = + combine(_taskData, visibleTaskIds) { tasks, visibleIds -> + tasks.filter { it.key.id in visibleIds } + } + val visibleThumbnailDataRequests: Flow> = + visibleTasks.map { + it.map { visibleTask -> + val taskCopy = Task(visibleTask).apply { thumbnail = visibleTask.thumbnail } + getThumbnailDataRequest(taskCopy) + } + } + return visibleThumbnailDataRequests.flatMapLatest { + thumbnailRequestFlows: List -> + if (thumbnailRequestFlows.isEmpty()) { + flowOf(emptyMap()) + } else { + combine(thumbnailRequestFlows) { it.toMap() } + } + } + } +} + +typealias ThumbnailDataRequest = Flow> diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt new file mode 100644 index 0000000000..55598f0a2d --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt @@ -0,0 +1,29 @@ +/* + * 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.data + +import com.android.launcher3.util.CancellableTask +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import java.util.function.Consumer + +interface TaskThumbnailDataSource { + fun updateThumbnailInBackground( + task: Task, + callback: Consumer + ): CancellableTask? +} diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java index 07f2d68869..8d99069c19 100644 --- a/quickstep/src/com/android/quickstep/util/DesktopTask.java +++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java @@ -21,7 +21,7 @@ import androidx.annotation.NonNull; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; -import java.util.ArrayList; +import java.util.List; /** * A {@link Task} container that can contain N number of tasks that are part of the desktop in @@ -30,9 +30,9 @@ import java.util.ArrayList; public class DesktopTask extends GroupTask { @NonNull - public final ArrayList tasks; + public final List tasks; - public DesktopTask(@NonNull ArrayList tasks) { + public DesktopTask(@NonNull List tasks) { super(tasks.get(0), null, null, TaskView.Type.DESKTOP); this.tasks = tasks; } @@ -52,6 +52,12 @@ public class DesktopTask extends GroupTask { return true; } + @Override + @NonNull + public List getTasks() { + return tasks; + } + @Override public DesktopTask copy() { return new DesktopTask(tasks); diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java index 7dd6afc55c..945ffe31f6 100644 --- a/quickstep/src/com/android/quickstep/util/GroupTask.java +++ b/quickstep/src/com/android/quickstep/util/GroupTask.java @@ -23,6 +23,10 @@ import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + /** * A {@link Task} container that can contain one or two tasks, depending on if the two tasks * are represented as an app-pair in the recents task list. @@ -61,6 +65,17 @@ public class GroupTask { return task2 != null; } + /** + * Returns a List of all the Tasks in this GroupTask + */ + public List getTasks() { + if (task2 == null) { + return Collections.singletonList(task1); + } else { + return Arrays.asList(task1, task2); + } + } + /** * Create a copy of this instance */ diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 4e5d646e27..5eee64d843 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -1141,6 +1141,7 @@ public abstract class RecentsView taskGroups) { + protected void applyLoadPlan(List taskGroups) { if (mPendingAnimation != null) { mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups)); return; diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt new file mode 100644 index 0000000000..eaeb513ea5 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +import com.android.quickstep.util.GroupTask +import java.util.function.Consumer + +class FakeRecentTasksDataSource : RecentTasksDataSource { + var taskList: List = listOf() + + override fun getTasks(callback: Consumer>?): Int { + callback?.accept(taskList) + return 0 + } + + fun seedTasks(tasks: List) { + taskList = tasks + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt new file mode 100644 index 0000000000..b66b7351bf --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +import android.graphics.Bitmap +import com.android.launcher3.util.CancellableTask +import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import java.util.function.Consumer +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class FakeTaskThumbnailDataSource : TaskThumbnailDataSource { + + val taskIdToBitmap: Map = (0..10).associateWith { mock() } + val taskIdToUpdatingTask: MutableMap Unit> = mutableMapOf() + var shouldLoadSynchronously: Boolean = true + + /** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */ + override fun updateThumbnailInBackground( + task: Task, + callback: Consumer + ): CancellableTask? { + val thumbnailData = mock() + whenever(thumbnailData.thumbnail).thenReturn(taskIdToBitmap[task.key.id]) + val wrappedCallback = { + task.thumbnail = thumbnailData + callback.accept(thumbnailData) + } + if (shouldLoadSynchronously) { + wrappedCallback() + } else { + taskIdToUpdatingTask[task.key.id] = wrappedCallback + } + return null + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt new file mode 100644 index 0000000000..c28a85a8f8 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +import android.content.ComponentName +import android.content.Intent +import com.android.quickstep.TaskIconCache +import com.android.quickstep.util.DesktopTask +import com.android.quickstep.util.GroupTask +import com.android.systemui.shared.recents.model.Task +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.mockito.kotlin.mock + +@OptIn(ExperimentalCoroutinesApi::class) +class TasksRepositoryTest { + private val tasks = (0..5).map(::createTaskWithId) + private val defaultTaskList = + listOf( + GroupTask(tasks[0]), + GroupTask(tasks[1], tasks[2], null), + DesktopTask(tasks.subList(3, 6)) + ) + private val recentsModel = FakeRecentTasksDataSource() + private val taskThumbnailDataSource = FakeTaskThumbnailDataSource() + private val taskIconCache = mock() + + private val systemUnderTest = + TasksRepository(recentsModel, taskThumbnailDataSource, taskIconCache) + + @Test + fun getAllTaskDataReturnsFlattenedListOfTasks() = runTest { + recentsModel.seedTasks(defaultTaskList) + + assertThat(systemUnderTest.getAllTaskData(forceRefresh = true).first()).isEqualTo(tasks) + } + + @Test + fun getTaskDataByIdReturnsSpecificTask() = runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + assertThat(systemUnderTest.getTaskDataById(2).first()).isEqualTo(tasks[2]) + } + + @Test + fun setVisibleTasksPopulatesThumbnails() = runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(listOf(1, 2)) + + // .drop(1) to ignore initial null content before from thumbnail was loaded. + assertThat(systemUnderTest.getTaskDataById(1).drop(1).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap1) + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + } + + @Test + fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(listOf(1, 2)) + + // .drop(1) to ignore initial null content before from thumbnail was loaded. + assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + + // Prevent new loading of Bitmaps + taskThumbnailDataSource.shouldLoadSynchronously = false + systemUnderTest.setVisibleTasks(listOf(2, 3)) + + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + } + + @Test + fun retrievedThumbnailsAreDiscardedWhenTaskBecomesInvisible() = runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(listOf(1, 2)) + + // .drop(1) to ignore initial null content before from thumbnail was loaded. + assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + + // Prevent new loading of Bitmaps + taskThumbnailDataSource.shouldLoadSynchronously = false + systemUnderTest.setVisibleTasks(listOf(0, 1)) + + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull() + } + + @Test + fun retrievedThumbnailsCauseEmissionOnTaskDataFlow() = runTest { + // Setup fakes + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + taskThumbnailDataSource.shouldLoadSynchronously = false + + // Setup TasksRepository + systemUnderTest.getAllTaskData(forceRefresh = true) + systemUnderTest.setVisibleTasks(listOf(1, 2)) + + // Assert there is no bitmap in first emission + val taskFlow = systemUnderTest.getTaskDataById(2) + val taskFlowValuesList = mutableListOf() + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + taskFlow.toList(taskFlowValuesList) + } + assertThat(taskFlowValuesList[0]!!.thumbnail).isNull() + + // Simulate bitmap loading after first emission + taskThumbnailDataSource.taskIdToUpdatingTask.getValue(2).invoke() + + // Check for second emission + assertThat(taskFlowValuesList[1]!!.thumbnail!!.thumbnail).isEqualTo(bitmap2) + } + + private fun createTaskWithId(taskId: Int) = + Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)) +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt index 0de5f197ea..aa08ca4c05 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt @@ -31,7 +31,6 @@ import com.android.launcher3.logging.StatsLogManager.StatsLogger import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.statehandlers.DepthController import com.android.launcher3.statemanager.StateManager -import com.android.launcher3.statemanager.StatefulActivity import com.android.launcher3.util.ComponentKey import com.android.launcher3.util.SplitConfigurationOptions import com.android.quickstep.RecentsModel @@ -121,7 +120,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonMatchingComponent), false /* findExactPairMatch */, @@ -174,7 +173,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent), false /* findExactPairMatch */, @@ -215,7 +214,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonPrimaryUserComponent), false /* findExactPairMatch */, @@ -271,7 +270,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonPrimaryUserComponent), false /* findExactPairMatch */, @@ -324,7 +323,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent), false /* findExactPairMatch */, @@ -378,7 +377,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonMatchingComponent, matchingComponent), false /* findExactPairMatch */, @@ -431,7 +430,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent, matchingComponent), false /* findExactPairMatch */, @@ -497,7 +496,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent, matchingComponent), false /* findExactPairMatch */, @@ -549,7 +548,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent2, matchingComponent), true /* findExactPairMatch */, diff --git a/quickstep/tests/multivalentTestsForDeviceless b/quickstep/tests/multivalentTestsForDeviceless deleted file mode 120000 index fa0fabf27b..0000000000 --- a/quickstep/tests/multivalentTestsForDeviceless +++ /dev/null @@ -1 +0,0 @@ -./multivalentTests \ No newline at end of file diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt index 4fafde8e80..5b567101b6 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt +++ b/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt @@ -86,7 +86,8 @@ class DesktopTaskbarRunningAppsControllerTest : TaskbarBaseTestCase() { val newHotseatItems = taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) - assertThat(newHotseatItems.map { it?.targetPackage }).isEqualTo(hotseatPackages) + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(hotseatPackages) } @Test @@ -119,7 +120,8 @@ class DesktopTaskbarRunningAppsControllerTest : TaskbarBaseTestCase() { RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, ) - assertThat(newHotseatItems.map { it?.targetPackage }).isEqualTo(expectedPackages) + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(expectedPackages) } @Test @@ -144,7 +146,8 @@ class DesktopTaskbarRunningAppsControllerTest : TaskbarBaseTestCase() { RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, ) - assertThat(newHotseatItems.map { it?.targetPackage }).isEqualTo(expectedPackages) + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(expectedPackages) } @Test @@ -155,7 +158,8 @@ class DesktopTaskbarRunningAppsControllerTest : TaskbarBaseTestCase() { whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) taskbarRunningAppsController.updateRunningApps() - assertThat(taskbarRunningAppsController.runningApps).isEqualTo(emptySet()) + assertThat(taskbarRunningAppsController.runningApps).isEmpty() + assertThat(taskbarRunningAppsController.minimizedApps).isEmpty() } @Test @@ -167,7 +171,28 @@ class DesktopTaskbarRunningAppsControllerTest : TaskbarBaseTestCase() { taskbarRunningAppsController.updateRunningApps() assertThat(taskbarRunningAppsController.runningApps) - .isEqualTo(setOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) + .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) + assertThat(taskbarRunningAppsController.minimizedApps).isEmpty() + } + + @Test + fun getMinimizedApps_inDesktopMode_returnsAllAppsRunningAndInvisibleAppsMinimized() { + setInDesktopMode(true) + val runningTasks = + ArrayList( + listOf( + createDesktopTaskInfo(RUNNING_APP_PACKAGE_1) { isVisible = true }, + createDesktopTaskInfo(RUNNING_APP_PACKAGE_2) { isVisible = true }, + createDesktopTaskInfo(RUNNING_APP_PACKAGE_3) { isVisible = false }, + ) + ) + whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) + taskbarRunningAppsController.updateRunningApps() + + assertThat(taskbarRunningAppsController.runningApps) + .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3) + assertThat(taskbarRunningAppsController.minimizedApps) + .containsExactly(RUNNING_APP_PACKAGE_3) } private fun createHotseatItemsFromPackageNames(packageNames: List): List { @@ -180,11 +205,15 @@ class DesktopTaskbarRunningAppsControllerTest : TaskbarBaseTestCase() { return ArrayList(packageNames.map { createDesktopTaskInfo(packageName = it) }) } - private fun createDesktopTaskInfo(packageName: String): RunningTaskInfo { + private fun createDesktopTaskInfo( + packageName: String, + init: RunningTaskInfo.() -> Unit = { isVisible = true }, + ): RunningTaskInfo { return RunningTaskInfo().apply { taskId = nextTaskId++ configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM realActivity = ComponentName(packageName, "TestActivity") + init() } } diff --git a/res/values/dimens.xml b/res/values/dimens.xml index e31a35ff6f..27411581a9 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -416,6 +416,9 @@ 0dp 0dp 0dp + 0dp + 0dp + 0dp 0dp diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 2a8298f694..7d09164feb 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -186,9 +186,20 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, // These fields, related to showing running apps, are only used for Taskbar. private final Size mRunningAppIndicatorSize; private final int mRunningAppIndicatorTopMargin; + private final Size mMinimizedAppIndicatorSize; + private final int mMinimizedAppIndicatorTopMargin; private final Paint mRunningAppIndicatorPaint; private final Rect mRunningAppIconBounds = new Rect(); - private boolean mIsRunning; + private RunningAppState mRunningAppState; + + /** + * Various options for the running state of an app. + */ + public enum RunningAppState { + NOT_RUNNING, + RUNNING, + MINIMIZED, + } @ViewDebug.ExportedProperty(category = "launcher") private boolean mStayPressed; @@ -259,9 +270,16 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, mRunningAppIndicatorSize = new Size( getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_width), getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_height)); + mMinimizedAppIndicatorSize = new Size( + getResources().getDimensionPixelSize(R.dimen.taskbar_minimized_app_indicator_width), + getResources().getDimensionPixelSize( + R.dimen.taskbar_minimized_app_indicator_height)); mRunningAppIndicatorTopMargin = getResources().getDimensionPixelSize( R.dimen.taskbar_running_app_indicator_top_margin); + mMinimizedAppIndicatorTopMargin = + getResources().getDimensionPixelSize( + R.dimen.taskbar_minimized_app_indicator_top_margin); mRunningAppIndicatorPaint = new Paint(); mRunningAppIndicatorPaint.setColor(getResources().getColor( R.color.taskbar_running_app_indicator_color, context.getTheme())); @@ -414,8 +432,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, /** Updates whether the app this view represents is currently running. */ @UiThread - public void updateRunningState(boolean isRunning) { - mIsRunning = isRunning; + public void updateRunningState(RunningAppState runningAppState) { + mRunningAppState = runningAppState; } protected void setItemInfo(ItemInfoWithIcon itemInfo) { @@ -667,18 +685,20 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, /** Draws a line under the app icon if this is representing a running app in Desktop Mode. */ protected void drawRunningAppIndicatorIfNecessary(Canvas canvas) { - if (!mIsRunning || mDisplay != DISPLAY_TASKBAR) { + if (mRunningAppState == RunningAppState.NOT_RUNNING || mDisplay != DISPLAY_TASKBAR) { return; } getIconBounds(mRunningAppIconBounds); // TODO(b/333872717): update color, shape, and size of indicator - int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin; - canvas.drawRect( - mRunningAppIconBounds.centerX() - mRunningAppIndicatorSize.getWidth() / 2, - indicatorTop, - mRunningAppIconBounds.centerX() + mRunningAppIndicatorSize.getWidth() / 2, - indicatorTop + mRunningAppIndicatorSize.getHeight(), - mRunningAppIndicatorPaint); + boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED; + int indicatorTop = + mRunningAppIconBounds.bottom + (isMinimized ? mMinimizedAppIndicatorTopMargin + : mRunningAppIndicatorTopMargin); + final Size indicatorSize = + isMinimized ? mMinimizedAppIndicatorSize : mRunningAppIndicatorSize; + canvas.drawRect(mRunningAppIconBounds.centerX() - indicatorSize.getWidth() / 2, + indicatorTop, mRunningAppIconBounds.centerX() + indicatorSize.getWidth() / 2, + indicatorTop + indicatorSize.getHeight(), mRunningAppIndicatorPaint); } @Override diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index a667c9674b..78a3eedb47 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -32,6 +32,7 @@ import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTO import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE; import static com.android.launcher3.testing.shared.ResourceUtils.pxFromDp; import static com.android.launcher3.testing.shared.ResourceUtils.roundPxValueFromFloat; +import static com.android.wm.shell.Flags.enableTinyTaskbar; import android.annotation.SuppressLint; import android.content.Context; @@ -353,7 +354,7 @@ public class DeviceProfile { isTablet = info.isTablet(windowBounds); isPhone = !isTablet; isTwoPanels = isTablet && isMultiDisplay; - isTaskbarPresent = isTablet + isTaskbarPresent = (isTablet || (enableTinyTaskbar() && isGestureMode)) && WindowManagerProxy.INSTANCE.get(context).isTaskbarDrawnInProcess(); // Some more constants. diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java index df8f635525..cdbd0c0be7 100644 --- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java +++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java @@ -109,6 +109,13 @@ public class RecyclerViewFastScroller extends View { private float mLastTouchY; private boolean mIsDragging; + /** + * Tracks whether a keyboard hide request has been sent due to downward scrolling. + *

+ * Set to true when scrolling down and reset when scrolling up to prevents redundant hide + * requests during continuous downward scrolls. + */ + private boolean mRequestedHideKeyboard; private boolean mIsThumbDetached; private final boolean mCanThumbDetach; private boolean mIgnoreDragGesture; @@ -241,6 +248,7 @@ public class RecyclerViewFastScroller extends View { public boolean handleTouchEvent(MotionEvent ev, Point offset) { int x = (int) ev.getX() - offset.x; int y = (int) ev.getY() - offset.y; + ActivityContext activityContext = ActivityContext.lookupContext(getContext()); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: @@ -248,6 +256,7 @@ public class RecyclerViewFastScroller extends View { mDownX = x; mDownY = mLastY = y; mDownTimeStampMillis = ev.getDownTime(); + mRequestedHideKeyboard = false; if ((Math.abs(mDy) < mDeltaThreshold && mRv.getScrollState() != SCROLL_STATE_IDLE)) { @@ -260,6 +269,15 @@ public class RecyclerViewFastScroller extends View { } break; case MotionEvent.ACTION_MOVE: + if (y > mLastY) { + if (!mRequestedHideKeyboard) { + activityContext.hideKeyboard(); + } + mRequestedHideKeyboard = true; + } else { + mRequestedHideKeyboard = false; + } + mLastY = y; int absDeltaY = Math.abs(y - mDownY); int absDeltaX = Math.abs(x - mDownX); @@ -294,7 +312,6 @@ public class RecyclerViewFastScroller extends View { } private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) { - ActivityContext.lookupContext(getContext()).hideKeyboard(); mIsDragging = true; if (mCanThumbDetach) { mIsThumbDetached = true; diff --git a/tests/multivalentTestsForDeviceless b/tests/multivalentTestsForDeviceless deleted file mode 120000 index 20ee34ada1..0000000000 --- a/tests/multivalentTestsForDeviceless +++ /dev/null @@ -1 +0,0 @@ -multivalentTests \ No newline at end of file