From 883a79db715258e92b738357771078548920df65 Mon Sep 17 00:00:00 2001 From: "Liam, Lee Pong Lam" Date: Mon, 20 May 2024 22:25:06 +0000 Subject: [PATCH 01/11] [Launcher3] Hide keyboard when fastscroller scrolling down and keyboard shown Implement keyboard hiding logic: Hide keyboard when scrolling down (y > mLastY) unless already requested (mRequestedHideKeyboard). Track hide request state to prevent redundant calls. result video: https://photos.app.goo.gl/e4dYUUiUN3BujNUz9 Bug: 333967597 Flag: EXEMPT bugfix Test: Manual Change-Id: I42b9d05dd5368b021577f12541248d434ee22495 --- .../views/RecyclerViewFastScroller.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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; From c228dbd2c13b6577874f46ec7f65efe7403809fa Mon Sep 17 00:00:00 2001 From: Alex Chau Date: Thu, 23 May 2024 11:13:50 +0100 Subject: [PATCH 02/11] Reset RecentsView when detached from window - onDetachedFromWindow is a sign RecentsView or even activity will be destroyed, if we don't reset RecentsAnimationController will be stuck without finsihing, causing live tile to be stuck on screen Bug: 339747262 Test: Swithcing apps (recents button, quick switch, OVerview) from Maps driving mode, live tile not stuck Flag: EXEMPT bugfix Change-Id: I990cfe0fc814c9125dbfbaf8a298f1b871e0194e --- quickstep/src/com/android/quickstep/views/RecentsView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 4e5d646e27..275734dbd3 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 Date: Fri, 17 May 2024 13:24:22 +0100 Subject: [PATCH 03/11] Update desktop mode to identify minimized tasks When a task is minimised, it becomes invisible but is still running. WMShell now signals to the launcher when a task's visibility changes. The task bar takes the visibility into account to know if a running task is, in fact, minimised. Test: atest NexusLauncherTests:DesktopTaskbarRunningAppsControllerTest Flag: com.android.window.flags.enable_desktop_windowing_taskbar_running_apps Bug: 333872717 Change-Id: Iaff6b1240d354bb3c4de8e4884948acf9bf40112 --- quickstep/res/values/dimens.xml | 3 ++ .../DesktopTaskbarRunningAppsController.kt | 54 ++++++++++++++----- .../taskbar/TaskbarModelCallbacks.java | 15 +++--- .../taskbar/TaskbarRecentAppsController.java | 5 ++ .../taskbar/TaskbarViewController.java | 20 ++++++- ...DesktopTaskbarRunningAppsControllerTest.kt | 41 +++++++++++--- res/values/dimens.xml | 3 ++ src/com/android/launcher3/BubbleTextView.java | 42 +++++++++++---- 8 files changed, 145 insertions(+), 38 deletions(-) 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/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/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 b4f8a475e0..cb6cdc5b7d 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 From e1eaefd537b582f9e3d23c0d9038a21376648462 Mon Sep 17 00:00:00 2001 From: Brian Isganitis Date: Tue, 21 May 2024 15:38:08 -0400 Subject: [PATCH 04/11] Initial prototype of tiny taskbar. Introducing isTinyTaskbar and using in a few places to show Taskbar on phones. Disabling for three button mode since the buttons will take up most of the space. Could also prototype adding meta key as a 4th button in this case if we feel like it. Flag: com.android.wm.shell.enable_tiny_taskbar Test: Manual Bug: 341784466 Change-Id: Iba07382d9bd5d14fc29ef080a390db561f6fd454 --- .../launcher3/taskbar/TaskbarActivityContext.java | 12 ++++++++++-- .../android/launcher3/taskbar/TaskbarManager.java | 2 +- src/com/android/launcher3/DeviceProfile.java | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) 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/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/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. From 4aa0b3f261c30bdc0f36e03e727c19321441e8a5 Mon Sep 17 00:00:00 2001 From: Brian Isganitis Date: Tue, 21 May 2024 17:57:50 -0400 Subject: [PATCH 05/11] Disable taskbar EDU for tiny taskbar. The tooltips are generally too large for phone mode, and since this is a prototype, let's disable them when in tiny mode :) Flag: com.android.wm.shell.enable_tiny_taskbar Test: Manual Bug: 341784466 Change-Id: I340485edf8bedc6b5d9501cb7c74b0864ae79630 --- .../launcher3/taskbar/TaskbarEduTooltipController.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From 6fd271bb8a04833b25bc23dca3de384e628073a6 Mon Sep 17 00:00:00 2001 From: Brian Isganitis Date: Thu, 23 May 2024 22:31:07 +0000 Subject: [PATCH 06/11] Revert "Add flag for Tiny Taskbar" This reverts commit 1a57e08d8492d0901a7cee5c976a120df616b026. Reason for revert: ag/27470277 flag instead Flag: com.android.wm.shell.enable_tiny_taskbar Test: None Bug: 341784466 Change-Id: I1a36c66eabb9c2ddac4595cec6836da222182351 --- aconfig/launcher.aconfig | 7 ------- 1 file changed, 7 deletions(-) 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" -} From 05682a059f32273679ebee7207b8752780873bf6 Mon Sep 17 00:00:00 2001 From: Winson Chung Date: Thu, 23 May 2024 06:54:03 +0000 Subject: [PATCH 07/11] Report gesture height for mandatory system gestures - We are currently reporting the content insets for the mandatory system gestures in taskbar, but for button nav we should actually be reporting zero, and for gesture nav we should either report the content height if the taskbar is pinned, or the gesture height otherwise as the bottom mandatory gesture inset Bug: 340134342 Test: atest android.systemui.cts.WindowInsetsBehaviorTests Change-Id: Ie4d56b62c903c273db95c19c9d34fcfe6c9ce486 --- .../taskbar/TaskbarInsetsController.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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) { From b9487c16ce079dd95ce5ad83be2ed8fe05c8f5a6 Mon Sep 17 00:00:00 2001 From: Bill Yi Date: Thu, 23 May 2024 20:52:20 -0700 Subject: [PATCH 08/11] Import translations. DO NOT MERGE ANYWHERE Auto-generated-cl: translation import Change-Id: I621e54f11fe617f515d5a68af4dc0555c4f12236 --- res/values-bg/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index c4364b4c73..6e16aaa75e 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -183,13 +183,13 @@ "Отмяна на паузата" "Филтър" "Неуспешно: %1$s" - "Лично пространство" + "Частно пространство" "Заключване и скриване на частните приложения" "Лично" - "Настройки за личното пространство" - "Заключване/отключване на личното пространство" + "Настройки за частното пространство" + "Заключване/отключване на частното пространство" "Заключване" - "Преминаване към личното пространство" + "Преминаване към частното пространство" "Инсталиране на приложения" "Инсталиране на приложения в частно пространство" "Препълване" From 21bb36fa6635bae7c8782490eb138c404f448755 Mon Sep 17 00:00:00 2001 From: Winson Chung Date: Thu, 23 May 2024 06:54:03 +0000 Subject: [PATCH 09/11] Report gesture height for mandatory system gestures - We are currently reporting the content insets for the mandatory system gestures in taskbar, but for button nav we should actually be reporting zero, and for gesture nav we should either report the content height if the taskbar is pinned, or the gesture height otherwise as the bottom mandatory gesture inset Fixes: 340134342 Test: atest android.systemui.cts.WindowInsetsBehaviorTests Change-Id: Ie4d56b62c903c273db95c19c9d34fcfe6c9ce486 Merged-In: Ie4d56b62c903c273db95c19c9d34fcfe6c9ce486 (cherry picked from commit 05682a059f32273679ebee7207b8752780873bf6) --- .../taskbar/TaskbarInsetsController.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt index 8dc81cf461..4f511c1bfd 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 @@ -218,8 +219,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) { From f6f34ca4412048fb74e38e12ef25454bb72b542c Mon Sep 17 00:00:00 2001 From: Uwais Ashraf Date: Mon, 20 May 2024 21:19:22 +0000 Subject: [PATCH 10/11] Add TasksRepository Bug: 334825222 Test: TasksRepositoryTest Flag: com.android.launcher3.enable_refactor_task_thumbnail Change-Id: I3e08dea7b205df54f8bef456ead6466aa2ce45c6 --- .../KeyboardQuickSwitchController.java | 6 +- .../android/quickstep/RecentTasksList.java | 4 +- .../com/android/quickstep/RecentsModel.java | 11 +- .../android/quickstep/TaskThumbnailCache.java | 7 +- .../fallback/FallbackRecentsView.java | 3 +- .../recents/data/RecentTasksDataSource.kt | 24 +++ .../quickstep/recents/data/TasksRepository.kt | 114 +++++++++++++ .../thumbnail/data/TaskThumbnailDataSource.kt | 29 ++++ .../android/quickstep/util/DesktopTask.java | 12 +- .../com/android/quickstep/util/GroupTask.java | 15 ++ .../android/quickstep/views/RecentsView.java | 2 +- .../recents/data/FakeRecentTasksDataSource.kt | 33 ++++ .../data/FakeTaskThumbnailDataSource.kt | 52 ++++++ .../recents/data/TasksRepositoryTest.kt | 150 ++++++++++++++++++ .../util/SplitSelectStateControllerTest.kt | 19 ++- 15 files changed, 456 insertions(+), 25 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/recents/data/RecentTasksDataSource.kt create mode 100644 quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt create mode 100644 quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt 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/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..8ee7dbc154 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -1715,7 +1715,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 */, From ba4fd32789fcf50a2948a536d34f2ad35c9e0bc5 Mon Sep 17 00:00:00 2001 From: Uwais Ashraf Date: Fri, 24 May 2024 15:51:04 +0000 Subject: [PATCH 11/11] Remove multivalentTestsForDeviceless symlink and make deviceless default Bug: 342146813 Test: Ran tests in studio Flag: EXEMPT test build files Change-Id: I0971a5b25a09069157bae51d518ddde502bd4e74 --- quickstep/tests/multivalentTestsForDeviceless | 1 - tests/multivalentTestsForDeviceless | 1 - 2 files changed, 2 deletions(-) delete mode 120000 quickstep/tests/multivalentTestsForDeviceless delete mode 120000 tests/multivalentTestsForDeviceless 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/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