Merge "Add flags for OneGrid project" into main
diff --git a/Android.bp b/Android.bp
index a0187e3..bcbd362 100644
--- a/Android.bp
+++ b/Android.bp
@@ -333,7 +333,7 @@
"com_android_wm_shell_flags_lib",
"dagger2",
"jsr330",
-
+ "com_android_systemui_shared_flags_lib",
],
manifest: "AndroidManifest-common.xml",
sdk_version: "current",
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index e11b00c..23733a4 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -39,3 +39,12 @@
bug: "353947137"
}
+flag {
+ name: "enable_overview_command_helper_timeout"
+ namespace: "launcher_overview"
+ description: "Enables OverviewCommandHelper new version with a timeout to prevent the queue to be unresponsive."
+ bug: "351122926"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/quickstep/res/layout/digital_wellbeing_toast.xml b/quickstep/res/layout/digital_wellbeing_toast.xml
index 3973e56..0551c12 100644
--- a/quickstep/res/layout/digital_wellbeing_toast.xml
+++ b/quickstep/res/layout/digital_wellbeing_toast.xml
@@ -27,4 +27,5 @@
android:textColor="?attr/materialColorOnSecondaryFixed"
android:textSize="14sp"
android:autoSizeTextType="uniform"
- android:autoSizeMaxTextSize="14sp"/>
\ No newline at end of file
+ android:autoSizeMaxTextSize="14sp"
+ android:visibility="gone"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index bdfd241..760bcdb 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -49,6 +49,5 @@
android:layout_width="wrap_content" />
<include layout="@layout/digital_wellbeing_toast"
- android:id="@+id/digital_wellbeing_toast"
- android:visibility="invisible"/>
+ android:id="@+id/digital_wellbeing_toast"/>
</com.android.quickstep.views.TaskView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index 00a990b..c36a45e 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -75,10 +75,8 @@
android:layout_width="wrap_content" />
<include layout="@layout/digital_wellbeing_toast"
- android:id="@+id/digital_wellbeing_toast"
- android:visibility="invisible"/>
+ android:id="@+id/digital_wellbeing_toast"/>
<include layout="@layout/digital_wellbeing_toast"
- android:id="@+id/bottomRight_digital_wellbeing_toast"
- android:visibility="invisible"/>
+ android:id="@+id/bottomRight_digital_wellbeing_toast"/>
</com.android.quickstep.views.GroupedTaskView>
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 8ceb77d..ce96556 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -36,14 +36,15 @@
import com.android.launcher3.R;
import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
import com.android.quickstep.util.BorderAnimator;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
-import java.util.function.Consumer;
-
import kotlin.Unit;
+import java.util.function.Consumer;
+
/**
* A view that displays a recent task during a keyboard quick switch.
*/
@@ -173,6 +174,61 @@
});
}
+ protected void setThumbnailsForSplitTasks(
+ @NonNull Task task1,
+ @Nullable Task task2,
+ @Nullable ThumbnailUpdateFunction thumbnailUpdateFunction,
+ @Nullable IconUpdateFunction iconUpdateFunction,
+ @Nullable SplitBounds splitBounds) {
+ setThumbnails(task1, task2, thumbnailUpdateFunction, iconUpdateFunction);
+
+ if (splitBounds == null) {
+ return;
+ }
+
+
+ final boolean isLeftRightSplit = !splitBounds.appsStackedVertically;
+ final float leftOrTopTaskPercent = isLeftRightSplit
+ ? splitBounds.leftTaskPercent : splitBounds.topTaskPercent;
+
+ ConstraintLayout.LayoutParams leftTopParams = (ConstraintLayout.LayoutParams)
+ mThumbnailView1.getLayoutParams();
+ ConstraintLayout.LayoutParams rightBottomParams = (ConstraintLayout.LayoutParams)
+ mThumbnailView2.getLayoutParams();
+
+ if (isLeftRightSplit) {
+ // Set thumbnail view ratio in left right split mode.
+ leftTopParams.width = 0; // Set width to 0dp, so it uses the constraint dimension ratio.
+ leftTopParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT;
+ leftTopParams.matchConstraintPercentWidth = leftOrTopTaskPercent;
+ leftTopParams.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID;
+ leftTopParams.rightToLeft = R.id.thumbnail_2;
+ mThumbnailView1.setLayoutParams(leftTopParams);
+
+ rightBottomParams.width = 0;
+ rightBottomParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT;
+ rightBottomParams.matchConstraintPercentWidth = 1 - leftOrTopTaskPercent;
+ rightBottomParams.leftToRight = R.id.thumbnail_1;
+ rightBottomParams.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID;
+ mThumbnailView2.setLayoutParams(rightBottomParams);
+ } else {
+ // Set thumbnail view ratio in top bottom split mode.
+ leftTopParams.height = 0;
+ leftTopParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT;
+ leftTopParams.matchConstraintPercentHeight = leftOrTopTaskPercent;
+ leftTopParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
+ leftTopParams.bottomToTop = R.id.thumbnail_2;
+ mThumbnailView1.setLayoutParams(leftTopParams);
+
+ rightBottomParams.height = 0;
+ rightBottomParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT;
+ rightBottomParams.matchConstraintPercentHeight = 1 - leftOrTopTaskPercent;
+ rightBottomParams.topToBottom = R.id.thumbnail_1;
+ rightBottomParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
+ mThumbnailView2.setLayoutParams(rightBottomParams);
+ }
+ }
+
private void applyThumbnail(
@Nullable ImageView thumbnailView,
@Nullable Task task,
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index a527c82..b4102a9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -214,11 +214,13 @@
groupTask.mSplitBounds == null
|| groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id
|| groupTask.task2 == null;
- currentTaskView.setThumbnails(
+
+ currentTaskView.setThumbnailsForSplitTasks(
firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2,
firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1,
updateTasks ? mViewCallbacks::updateThumbnailInBackground : null,
- updateTasks ? mViewCallbacks::updateIconInBackground : null);
+ updateTasks ? mViewCallbacks::updateIconInBackground : null,
+ groupTask.mSplitBounds);
previousTaskView = currentTaskView;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 47ae741..5f733b0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -1431,7 +1431,7 @@
&& !(foundTaskView instanceof DesktopTaskView)) {
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon");
- foundTaskView.launchTasks();
+ foundTaskView.launchWithAnimation();
return;
}
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 55c1885..0b385d9 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -610,7 +610,7 @@
.append(" is missing."),
QUICK_SWITCH_FROM_HOME_FALLBACK);
}
- taskToLaunch.launchTask(success -> {
+ taskToLaunch.launchWithoutAnimation(success -> {
if (!success) {
getStateManager().goToState(OVERVIEW);
} else {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 6822f1b..b165cdd 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -209,7 +209,7 @@
TaskView taskView = recentsView.getRunningTaskView();
if (taskView != null) {
if (recentsView.isTaskViewFullyVisible(taskView)) {
- taskView.launchTasks();
+ taskView.launchWithAnimation();
} else {
recentsView.snapToPage(recentsView.indexOfChild(taskView));
}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 38d08e0..55489bb 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -2370,7 +2370,7 @@
ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
}
ActiveGestureLog.INSTANCE.addLog(nextTaskLog);
- nextTask.launchTask(success -> {
+ nextTask.launchWithoutAnimation(true, success -> {
resultCallback.accept(success);
if (success) {
if (hasTaskPreviouslyAppeared) {
@@ -2383,7 +2383,7 @@
}
}
return Unit.INSTANCE;
- }, true /* freezeTaskList */);
+ } /* freezeTaskList */);
} else {
mContainerInterface.onLaunchTaskFailed();
Toast.makeText(mContext, R.string.activity_not_available, LENGTH_SHORT).show();
diff --git a/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt b/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
index 904ed69..e6822ff 100644
--- a/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
+++ b/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
@@ -52,7 +52,11 @@
)
val lpnhTimeoutMs =
- propReader.get("LPNH_TIMEOUT_MS", 450, "Controls lpnh timeout in milliseconds")
+ propReader.get(
+ "LPNH_TIMEOUT_MS",
+ DEFAULT_LPNH_TIMEOUT_MS,
+ "Controls lpnh timeout in milliseconds"
+ )
val lpnhSlopPercentage =
propReader.get("LPNH_SLOP_PERCENTAGE", 100, "Controls touch slop percentage for lpnh")
@@ -172,5 +176,7 @@
@JvmStatic val configHelper by lazy { DeviceConfigHelper(::DeviceConfigWrapper) }
@JvmStatic fun get() = configHelper.config
+
+ const val DEFAULT_LPNH_TIMEOUT_MS = 450
}
}
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 8873275..c1f9963 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -25,16 +25,26 @@
import android.view.View
import androidx.annotation.BinderThread
import androidx.annotation.UiThread
+import androidx.annotation.VisibleForTesting
import com.android.internal.jank.Cuj
+import com.android.launcher3.Flags.enableOverviewCommandHelperTimeout
import com.android.launcher3.PagedView
import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.logger.LauncherAtom
import com.android.launcher3.logging.StatsLogManager
-import com.android.launcher3.logging.StatsLogManager.LauncherEvent.*
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT
import com.android.launcher3.util.Executors
import com.android.launcher3.util.RunnableList
+import com.android.launcher3.util.coroutines.DispatcherProvider
+import com.android.launcher3.util.coroutines.ProductionDispatchers
import com.android.quickstep.OverviewCommandHelper.CommandInfo.CommandStatus
-import com.android.quickstep.OverviewCommandHelper.CommandType.*
+import com.android.quickstep.OverviewCommandHelper.CommandType.HIDE
+import com.android.quickstep.OverviewCommandHelper.CommandType.HOME
+import com.android.quickstep.OverviewCommandHelper.CommandType.KEYBOARD_INPUT
+import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW
+import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE
import com.android.quickstep.util.ActiveGestureLog
import com.android.quickstep.views.RecentsView
import com.android.quickstep.views.RecentsViewContainer
@@ -43,13 +53,25 @@
import com.android.systemui.shared.system.InteractionJankMonitorWrapper
import java.io.PrintWriter
import java.util.concurrent.ConcurrentLinkedDeque
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
/** Helper class to handle various atomic commands for switching between Overview. */
-class OverviewCommandHelper(
+class OverviewCommandHelper
+@JvmOverloads
+constructor(
private val touchInteractionService: TouchInteractionService,
private val overviewComponentObserver: OverviewComponentObserver,
- private val taskAnimationManager: TaskAnimationManager
+ private val taskAnimationManager: TaskAnimationManager,
+ private val dispatcherProvider: DispatcherProvider = ProductionDispatchers,
) {
+ private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.default)
+
private val commandQueue = ConcurrentLinkedDeque<CommandInfo>()
/**
@@ -79,10 +101,10 @@
* dropped.
*/
@BinderThread
- fun addCommand(type: CommandType) {
+ fun addCommand(type: CommandType): CommandInfo? {
if (commandQueue.size >= MAX_QUEUE_SIZE) {
- Log.d(TAG, "commands queue is full ($commandQueue). command not added: $type")
- return
+ Log.d(TAG, "command not added: $type - queue is full ($commandQueue).")
+ return null
}
val command = CommandInfo(type)
@@ -90,8 +112,17 @@
Log.d(TAG, "command added: $command")
if (commandQueue.size == 1) {
- Executors.MAIN_EXECUTOR.execute { executeNext() }
+ Log.d(TAG, "execute: $command - queue size: ${commandQueue.size}")
+ if (enableOverviewCommandHelperTimeout()) {
+ coroutineScope.launch(dispatcherProvider.main) { processNextCommand() }
+ } else {
+ Executors.MAIN_EXECUTOR.execute { processNextCommand() }
+ }
+ } else {
+ Log.d(TAG, "not executed: $command - queue size: ${commandQueue.size}")
}
+
+ return command
}
fun canStartHomeSafely(): Boolean = commandQueue.isEmpty() || commandQueue.first().type == HOME
@@ -108,7 +139,7 @@
* completion (returns false).
*/
@UiThread
- private fun executeNext() {
+ private fun processNextCommand() {
val command: CommandInfo =
commandQueue.firstOrNull()
?: run {
@@ -119,12 +150,22 @@
command.status = CommandStatus.PROCESSING
Log.d(TAG, "executing command: $command")
- val result = executeCommand(command)
- Log.d(TAG, "command executed: $command with result: $result")
- if (result) {
- onCommandFinished(command)
+ if (enableOverviewCommandHelperTimeout()) {
+ coroutineScope.launch(dispatcherProvider.main) {
+ withTimeout(QUEUE_WAIT_DURATION_IN_MS) {
+ executeCommandSuspended(command)
+ ensureActive()
+ onCommandFinished(command)
+ }
+ }
} else {
- Log.d(TAG, "waiting for command callback: $command")
+ val result = executeCommand(command, onCallbackResult = { onCommandFinished(command) })
+ Log.d(TAG, "command executed: $command with result: $result")
+ if (result) {
+ onCommandFinished(command)
+ } else {
+ Log.d(TAG, "waiting for command callback: $command")
+ }
}
}
@@ -132,7 +173,9 @@
* Executes the task and returns true if next task can be executed. If false, then the next task
* is deferred until [.scheduleNextTask] is called
*/
- private fun executeCommand(command: CommandInfo): Boolean {
+ @VisibleForTesting
+ fun executeCommand(command: CommandInfo, onCallbackResult: () -> Unit): Boolean {
+ // This shouldn't happen if we execute 1 command per time.
if (waitForToggleCommandComplete && command.type == TOGGLE) {
Log.d(TAG, "executeCommand: $command - waiting for toggle command complete")
return true
@@ -141,15 +184,37 @@
val recentsView = visibleRecentsView
Log.d(TAG, "executeCommand: $command - visibleRecentsView: $recentsView")
return if (recentsView != null) {
- executeWhenRecentsIsVisible(command, recentsView)
+ executeWhenRecentsIsVisible(command, recentsView, onCallbackResult)
} else {
- executeWhenRecentsIsNotVisible(command)
+ executeWhenRecentsIsNotVisible(command, onCallbackResult)
}
}
+ /**
+ * Executes the task and returns true if next task can be executed. If false, then the next task
+ * is deferred until [.scheduleNextTask] is called
+ */
+ private suspend fun executeCommandSuspended(command: CommandInfo) =
+ suspendCancellableCoroutine { continuation ->
+ fun processResult(isCompleted: Boolean) {
+ Log.d(TAG, "command executed: $command with result: $isCompleted")
+ if (isCompleted) {
+ continuation.resume(Unit)
+ } else {
+ Log.d(TAG, "waiting for command callback: $command")
+ }
+ }
+
+ val result = executeCommand(command, onCallbackResult = { processResult(true) })
+ processResult(result)
+
+ continuation.invokeOnCancellation { cancelCommand(command, it) }
+ }
+
private fun executeWhenRecentsIsVisible(
command: CommandInfo,
recentsView: RecentsView<*, *>,
+ onCallbackResult: () -> Unit,
): Boolean =
when (command.type) {
SHOW -> true // already visible
@@ -161,7 +226,7 @@
keyboardTaskFocusIndex = PagedView.INVALID_PAGE
val currentPage = recentsView.nextPage
val taskView = recentsView.getTaskViewAt(currentPage)
- launchTask(recentsView, taskView, command)
+ launchTask(recentsView, taskView, command, onCallbackResult)
}
}
TOGGLE -> {
@@ -171,7 +236,7 @@
} else {
recentsView.nextTaskView ?: recentsView.runningTaskView
}
- launchTask(recentsView, taskView, command)
+ launchTask(recentsView, taskView, command, onCallbackResult)
}
HOME -> {
recentsView.startHome()
@@ -182,19 +247,20 @@
private fun launchTask(
recents: RecentsView<*, *>,
taskView: TaskView?,
- command: CommandInfo
+ command: CommandInfo,
+ onCallbackResult: () -> Unit
): Boolean {
var callbackList: RunnableList? = null
if (taskView != null) {
waitForToggleCommandComplete = true
taskView.isEndQuickSwitchCuj = true
- callbackList = taskView.launchTasks()
+ callbackList = taskView.launchWithAnimation()
}
if (callbackList != null) {
callbackList.add {
Log.d(TAG, "launching task callback: $command")
- onCommandFinished(command)
+ onCallbackResult()
waitForToggleCommandComplete = false
}
Log.d(TAG, "launching task - waiting for callback: $command")
@@ -206,7 +272,10 @@
}
}
- private fun executeWhenRecentsIsNotVisible(command: CommandInfo): Boolean {
+ private fun executeWhenRecentsIsNotVisible(
+ command: CommandInfo,
+ onCallbackResult: () -> Unit
+ ): Boolean {
val recentsViewContainer = activityInterface.getCreatedContainer() as? RecentsViewContainer
val recentsView: RecentsView<*, *>? = recentsViewContainer?.getOverviewPanel()
val deviceProfile = recentsViewContainer?.getDeviceProfile()
@@ -263,7 +332,7 @@
Log.d(TAG, "switching to Overview state - onAnimationEnd: $command")
super.onAnimationEnd(animation)
onRecentsViewFocusUpdated(command)
- onCommandFinished(command)
+ onCallbackResult()
}
}
if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
@@ -289,7 +358,7 @@
command.createTime
)
interactionHandler.setGestureEndCallback {
- onTransitionComplete(command, interactionHandler)
+ onTransitionComplete(command, interactionHandler, onCallbackResult)
}
interactionHandler.initWhenReady("OverviewCommandHelper: command.type=${command.type}")
@@ -321,11 +390,6 @@
}
}
- // TODO(b/361768912): Dead code. Remove or update after this bug is fixed.
- // if (visibleRecentsView != null) {
- // visibleRecentsView.moveRunningTaskToFront();
- // }
-
if (taskAnimationManager.isRecentsAnimationRunning) {
command.setAnimationCallbacks(
taskAnimationManager.continueRecentsAnimation(gestureState)
@@ -351,29 +415,40 @@
return false
}
- private fun onTransitionComplete(command: CommandInfo, handler: AbsSwipeUpHandler<*, *, *>) {
+ private fun onTransitionComplete(
+ command: CommandInfo,
+ handler: AbsSwipeUpHandler<*, *, *>,
+ onCommandResult: () -> Unit
+ ) {
Log.d(TAG, "switching via recents animation - onTransitionComplete: $command")
command.removeListener(handler)
Trace.endAsyncSection(TRANSITION_NAME, 0)
onRecentsViewFocusUpdated(command)
- onCommandFinished(command)
+ onCommandResult()
}
/** Called when the command finishes execution. */
private fun onCommandFinished(command: CommandInfo) {
command.status = CommandStatus.COMPLETED
- if (commandQueue.first() !== command) {
+ if (commandQueue.firstOrNull() !== command) {
Log.d(
TAG,
"next task not scheduled. First pending command type " +
- "is ${commandQueue.first()} - command type is: $command"
+ "is ${commandQueue.firstOrNull()} - command type is: $command"
)
return
}
Log.d(TAG, "command executed successfully! $command")
commandQueue.remove(command)
- executeNext()
+ processNextCommand()
+ }
+
+ private fun cancelCommand(command: CommandInfo, throwable: Throwable?) {
+ command.status = CommandStatus.CANCELED
+ Log.e(TAG, "command cancelled: $command - $throwable")
+ commandQueue.remove(command)
+ processNextCommand()
}
private fun updateRecentsViewFocus(command: CommandInfo) {
@@ -447,7 +522,8 @@
pw.println(" waitForToggleCommandComplete=$waitForToggleCommandComplete")
}
- private data class CommandInfo(
+ @VisibleForTesting
+ data class CommandInfo(
val type: CommandType,
var status: CommandStatus = CommandStatus.IDLE,
val createTime: Long = SystemClock.elapsedRealtime(),
@@ -468,7 +544,8 @@
enum class CommandStatus {
IDLE,
PROCESSING,
- COMPLETED
+ COMPLETED,
+ CANCELED
}
}
@@ -489,5 +566,6 @@
* should be enough. We'll toss in one more because we're kind hearted.
*/
private const val MAX_QUEUE_SIZE = 3
+ private const val QUEUE_WAIT_DURATION_IN_MS = 5000L
}
}
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 9e6e2f3..785666f 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -431,7 +431,7 @@
@Override
public void onClick(View view) {
- if (mTaskView.launchTaskAnimated() != null) {
+ if (mTaskView.launchAsStaticTile() != null) {
SystemUiProxy.INSTANCE.get(mTarget.asContext()).startScreenPinning(
mTaskView.getFirstTask().key.id);
}
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index 32d9052..cc022b2 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -535,6 +535,18 @@
int parentWidth, int parentHeight, SplitBounds splitBoundsConfig,
DeviceProfile dp, boolean isRtl) {
int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
+
+ FrameLayout.LayoutParams primaryParams =
+ (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams();
+ FrameLayout.LayoutParams secondaryParams =
+ (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
+
+ // Reset margin and translations that aren't used in this method, but are used in other
+ // `RecentsPagedOrientationHandler` variants.
+ secondaryParams.topMargin = 0;
+ primaryParams.topMargin = spaceAboveSnapshot;
+ primarySnapshot.setTranslationY(0);
+
int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
float dividerScale = splitBoundsConfig.appsStackedVertically
? splitBoundsConfig.dividerHeightPercent
@@ -552,24 +564,14 @@
secondarySnapshot.setTranslationX(translationX);
primarySnapshot.setTranslationX(0);
}
- secondarySnapshot.setTranslationY(spaceAboveSnapshot);
- // Reset unused translations
- primarySnapshot.setTranslationY(0);
+ secondarySnapshot.setTranslationY(spaceAboveSnapshot);
} else {
float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale);
float translationY = taskViewSizes.first.y + spaceAboveSnapshot + finalDividerHeight;
secondarySnapshot.setTranslationY(translationY);
- FrameLayout.LayoutParams primaryParams =
- (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams();
- FrameLayout.LayoutParams secondaryParams =
- (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
- secondaryParams.topMargin = 0;
- primaryParams.topMargin = spaceAboveSnapshot;
-
- // Reset unused translations
- primarySnapshot.setTranslationY(0);
+ // Reset unused translations.
secondarySnapshot.setTranslationX(0);
primarySnapshot.setTranslationX(0);
}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 41add54..6db0923 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -262,7 +262,7 @@
}
Log.d(
TAG,
- "launchTaskAnimated - launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated"
+ "launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated"
)
// Callbacks get run from recentsView for case when recents animation already running
@@ -270,11 +270,12 @@
return endCallback
}
- override fun launchTaskAnimated() = launchTaskWithDesktopController(animated = true)
+ override fun launchAsStaticTile() = launchTaskWithDesktopController(animated = true)
- override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
- launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(false)
- }
+ override fun launchWithoutAnimation(
+ isQuickSwitch: Boolean,
+ callback: (launched: Boolean) -> Unit
+ ) = launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(false)
// Desktop tile can't be in split screen
override fun confirmSecondSplitSelectApp(): Boolean = false
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
index f0fdd81..7b97c23 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
@@ -39,6 +39,7 @@
import androidx.annotation.VisibleForTesting
import androidx.core.util.component1
import androidx.core.util.component2
+import androidx.core.view.isVisible
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.util.Executors
@@ -108,18 +109,18 @@
}
private fun setNoLimit() {
+ isVisible = false
hasLimit = false
- setContentDescription(appUsageLimitTimeMs = -1, appRemainingTimeMs = -1)
- visibility = INVISIBLE
appRemainingTimeMs = -1
+ setContentDescription(appUsageLimitTimeMs = -1, appRemainingTimeMs = -1)
}
private fun setLimit(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
- this.appRemainingTimeMs = appRemainingTimeMs
+ isVisible = true
hasLimit = true
- text = Utilities.prefixTextWithIcon(context, R.drawable.ic_hourglass_top, getBannerText())
- visibility = VISIBLE
+ this.appRemainingTimeMs = appRemainingTimeMs
setContentDescription(appUsageLimitTimeMs, appRemainingTimeMs)
+ text = Utilities.prefixTextWithIcon(context, R.drawable.ic_hourglass_top, getBannerText())
}
private fun setContentDescription(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
@@ -172,7 +173,7 @@
/** Mark the DWB toast as destroyed and hide it. */
fun destroy() {
- visibility = INVISIBLE
+ isVisible = false
isDestroyed = true
}
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index 4fae01e..3fd1a6b 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -218,11 +218,7 @@
invalidate()
}
- override fun launchTaskAnimated(): RunnableList? {
- if (taskContainers.isEmpty()) {
- Log.d(TAG, "launchTaskAnimated - task is not bound")
- return null
- }
+ override fun launchAsStaticTile(): RunnableList? {
val recentsView = recentsView ?: return null
val endCallback = RunnableList()
// Callbacks run from remote animation when recents animation not currently running
@@ -241,8 +237,11 @@
return endCallback
}
- override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
- launchTaskInternal(isQuickSwitch, false, callback /*launchingExistingTaskview*/)
+ override fun launchWithoutAnimation(
+ isQuickSwitch: Boolean,
+ callback: (launched: Boolean) -> Unit
+ ) {
+ launchTaskInternal(isQuickSwitch, launchingExistingTaskView = false, callback)
}
/**
@@ -266,7 +265,10 @@
isQuickSwitch,
snapPosition
)
- Log.d(TAG, "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}")
+ Log.d(
+ TAG,
+ "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}, launchingExistingTaskView: $launchingExistingTaskView"
+ )
}
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 226ecf5..709e0aa 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -635,13 +635,16 @@
@Override
public void onTaskRemoved(int taskId) {
if (!mHandleTaskStackChanges) {
+ Log.d(TAG, "onTaskRemoved: " + taskId + ", not handling task stack changes");
return;
}
TaskView taskView = getTaskViewByTaskId(taskId);
if (taskView == null) {
+ Log.d(TAG, "onTaskRemoved: " + taskId + ", no associated TaskView");
return;
}
+ Log.d(TAG, "onTaskRemoved: " + taskId);
Task.TaskKey taskKey = taskView.getFirstTask().key;
UI_HELPER_EXECUTOR.execute(new CancellableTask<>(
() -> PackageManagerWrapper.getInstance()
@@ -2138,6 +2141,7 @@
boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow()
&& getWindowVisibility() == VISIBLE;
if (handleTaskStackChanges != mHandleTaskStackChanges) {
+ Log.d(TAG, "updateTaskStackListenerState: " + handleTaskStackChanges);
mHandleTaskStackChanges = handleTaskStackChanges;
if (handleTaskStackChanges) {
reloadIfNeeded();
@@ -2731,9 +2735,12 @@
if (!mModel.isTaskListValid(mTaskListChangeId)) {
mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState
.getFilter(mFilterState.getPackageNameToFilter()));
+ Log.d(TAG, "reloadIfNeeded - getTasks: " + mTaskListChangeId);
if (enableRefactorTaskThumbnail()) {
mRecentsViewModel.refreshAllTaskData();
}
+ } else {
+ Log.d(TAG, "reloadIfNeeded - task list still valid: " + mTaskListChangeId);
}
}
@@ -4382,8 +4389,10 @@
private void dismissTask(int taskId) {
TaskView taskView = getTaskViewByTaskId(taskId);
if (taskView == null) {
+ Log.d(TAG, "dismissTask: " + taskId + ", no associated TaskView");
return;
}
+ Log.d(TAG, "dismissTask: " + taskId);
dismissTask(taskView, true /* animate */, false /* removeTask */);
}
@@ -5485,7 +5494,7 @@
finishRecentsAnimation(false /* toRecents */, null);
onTaskLaunchAnimationEnd(true /* success */);
} else {
- taskView.launchTask(this::onTaskLaunchAnimationEnd);
+ taskView.launchWithoutAnimation(this::onTaskLaunchAnimationEnd);
}
mContainer.getStatsLogManager().logger().withItemInfo(taskView.getFirstItemInfo())
.log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index d34a93b..13c4f78 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -177,6 +177,7 @@
addAccessibleChildToList(iconView.asView(), outChildren)
addAccessibleChildToList(snapshotView, outChildren)
showWindowsView?.let { addAccessibleChildToList(it, outChildren) }
+ digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) }
}
private fun addAccessibleChildToList(view: View, outChildren: ArrayList<View>) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 5614af6..601dae8 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -1003,7 +1003,7 @@
return
}
val callbackList =
- launchTasks()?.apply {
+ launchWithAnimation()?.apply {
add {
Log.d("b/310064698", "${taskIds.contentToString()} - onClick - launchCompleted")
}
@@ -1015,12 +1015,106 @@
.log(LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP)
}
+ /** Launch of the current task (both live and inactive tasks) with an animation. */
+ fun launchWithAnimation(): RunnableList? {
+ return if (isRunningTask && recentsView?.remoteTargetHandles != null) {
+ launchAsLiveTile()
+ } else {
+ launchAsStaticTile()
+ }
+ }
+
+ private fun launchAsLiveTile(): RunnableList? {
+ val recentsView = recentsView ?: return null
+ val remoteTargetHandles = recentsView.remoteTargetHandles
+ if (!isClickableAsLiveTile) {
+ Log.e(
+ TAG,
+ "launchAsLiveTile - TaskView is not clickable as a live tile; returning to home: ${taskIds.contentToString()}"
+ )
+ return null
+ }
+ isClickableAsLiveTile = false
+ val targets =
+ if (remoteTargetHandles.size == 1) {
+ remoteTargetHandles[0].transformParams.targetSet
+ } else {
+ val apps =
+ remoteTargetHandles.flatMap { it.transformParams.targetSet.apps.asIterable() }
+ val wallpapers =
+ remoteTargetHandles.flatMap {
+ it.transformParams.targetSet.wallpapers.asIterable()
+ }
+ RemoteAnimationTargets(
+ apps.toTypedArray(),
+ wallpapers.toTypedArray(),
+ remoteTargetHandles[0].transformParams.targetSet.nonApps,
+ remoteTargetHandles[0].transformParams.targetSet.targetMode
+ )
+ }
+ if (targets == null) {
+ // If the recents animation is cancelled somehow between the parent if block and
+ // here, try to launch the task as a non live tile task.
+ val runnableList = launchAsStaticTile()
+ if (runnableList == null) {
+ Log.e(
+ TAG,
+ "launchAsLiveTile - Recents animation cancelled and cannot launch task as non-live tile; returning to home: ${taskIds.contentToString()}"
+ )
+ }
+ isClickableAsLiveTile = true
+ return runnableList
+ }
+ TestLogging.recordEvent(
+ TestProtocol.SEQUENCE_MAIN,
+ "composeRecentsLaunchAnimator",
+ taskIds.contentToString()
+ )
+ val runnableList = RunnableList()
+ with(AnimatorSet()) {
+ TaskViewUtils.composeRecentsLaunchAnimator(
+ this,
+ this@TaskView,
+ targets.apps,
+ targets.wallpapers,
+ targets.nonApps,
+ true /* launcherClosing */,
+ recentsView.stateManager,
+ recentsView,
+ recentsView.depthController
+ )
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animator: Animator) {
+ if (taskContainers.any { it.task.key.displayId != rootViewDisplayId }) {
+ launchAsStaticTile()
+ }
+ isClickableAsLiveTile = true
+ runEndCallback()
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ runEndCallback()
+ }
+
+ private fun runEndCallback() {
+ runnableList.executeAllAndDestroy()
+ }
+ }
+ )
+ start()
+ }
+ Log.d(TAG, "launchAsLiveTile - composeRecentsLaunchAnimator: ${taskIds.contentToString()}")
+ recentsView.onTaskLaunchedInLiveTileMode()
+ return runnableList
+ }
+
/**
* Starts the task associated with this view and animates the startup.
*
* @return CompletionStage to indicate the animation completion or null if the launch failed.
*/
- open fun launchTaskAnimated(): RunnableList? {
+ open fun launchAsStaticTile(): RunnableList? {
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN,
"startActivityFromRecentsAsync",
@@ -1036,7 +1130,7 @@
) {
Log.d(
TAG,
- "launchTaskAnimated - startActivityFromRecents: ${taskIds.contentToString()}"
+ "launchAsStaticTile - startActivityFromRecents: ${taskIds.contentToString()}"
)
ActiveGestureLog.INSTANCE.trackEvent(
ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED
@@ -1060,18 +1154,17 @@
recentsView.addSideTaskLaunchCallback(opts.onEndCallback)
return opts.onEndCallback
} else {
- notifyTaskLaunchFailed()
+ notifyTaskLaunchFailed("launchAsStaticTile")
return null
}
}
/** Starts the task associated with this view without any animation */
- fun launchTask(callback: (launched: Boolean) -> Unit) {
- launchTask(callback, isQuickSwitch = false)
- }
-
- /** Starts the task associated with this view without any animation */
- open fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
+ @JvmOverloads
+ open fun launchWithoutAnimation(
+ isQuickSwitch: Boolean = false,
+ callback: (launched: Boolean) -> Unit
+ ) {
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN,
"startActivityFromRecentsAsync",
@@ -1084,7 +1177,7 @@
// gesture launcher is in the background state, vs other launches which are in
// the actual overview state
failureListener.register(container, firstContainer.task.key.id) {
- notifyTaskLaunchFailed()
+ notifyTaskLaunchFailed("launchWithoutAnimation")
recentsView?.let {
// Disable animations for now, as it is an edge case and the app usually
// covers launcher and also any state transition animation also gets
@@ -1128,103 +1221,20 @@
// otherwise, wait for the animation start callback from the activity options
// above
Executors.MAIN_EXECUTOR.post {
- notifyTaskLaunchFailed()
+ notifyTaskLaunchFailed("launchTask")
callback(false)
}
}
- Log.d(TAG, "launchTask - startActivityFromRecents: ${taskIds.contentToString()}")
+ Log.d(
+ TAG,
+ "launchWithoutAnimation - startActivityFromRecents: ${taskIds.contentToString()}"
+ )
}
}
- /** Launch of the current task (both live and inactive tasks) with an animation. */
- fun launchTasks(): RunnableList? {
- val recentsView = recentsView ?: return null
- val remoteTargetHandles = recentsView.mRemoteTargetHandles
- if (!isRunningTask || remoteTargetHandles == null) {
- return launchTaskAnimated()
- }
- if (!isClickableAsLiveTile) {
- Log.e(TAG, "TaskView is not clickable as a live tile; returning to home.")
- return null
- }
- isClickableAsLiveTile = false
- val targets =
- if (remoteTargetHandles.size == 1) {
- remoteTargetHandles[0].transformParams.targetSet
- } else {
- val apps =
- remoteTargetHandles.flatMap { it.transformParams.targetSet.apps.asIterable() }
- val wallpapers =
- remoteTargetHandles.flatMap {
- it.transformParams.targetSet.wallpapers.asIterable()
- }
- RemoteAnimationTargets(
- apps.toTypedArray(),
- wallpapers.toTypedArray(),
- remoteTargetHandles[0].transformParams.targetSet.nonApps,
- remoteTargetHandles[0].transformParams.targetSet.targetMode
- )
- }
- if (targets == null) {
- // If the recents animation is cancelled somehow between the parent if block and
- // here, try to launch the task as a non live tile task.
- val runnableList = launchTaskAnimated()
- if (runnableList == null) {
- Log.e(
- TAG,
- "Recents animation cancelled and cannot launch task as non-live tile" +
- "; returning to home"
- )
- }
- isClickableAsLiveTile = true
- return runnableList
- }
- TestLogging.recordEvent(
- TestProtocol.SEQUENCE_MAIN,
- "composeRecentsLaunchAnimator",
- taskIds.contentToString()
- )
- val runnableList = RunnableList()
- with(AnimatorSet()) {
- TaskViewUtils.composeRecentsLaunchAnimator(
- this,
- this@TaskView,
- targets.apps,
- targets.wallpapers,
- targets.nonApps,
- true /* launcherClosing */,
- recentsView.stateManager,
- recentsView,
- recentsView.depthController
- )
- addListener(
- object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animator: Animator) {
- if (taskContainers.any { it.task.key.displayId != rootViewDisplayId }) {
- launchTaskAnimated()
- }
- isClickableAsLiveTile = true
- runEndCallback()
- }
-
- override fun onAnimationCancel(animation: Animator) {
- runEndCallback()
- }
-
- private fun runEndCallback() {
- runnableList.executeAllAndDestroy()
- }
- }
- )
- start()
- }
- Log.d(TAG, "launchTasks - composeRecentsLaunchAnimator: ${taskIds.contentToString()}")
- recentsView.onTaskLaunchedInLiveTileMode()
- return runnableList
- }
-
- private fun notifyTaskLaunchFailed() {
- val sb = StringBuilder("Failed to launch task \n")
+ private fun notifyTaskLaunchFailed(launchMethod: String) {
+ val sb =
+ StringBuilder("$launchMethod - Failed to launch task: ${taskIds.contentToString()}\n")
taskContainers.forEach {
sb.append("(task=${it.task.key.baseIntent} userId=${it.task.key.userId})\n")
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
new file mode 100644
index 0000000..0ae710f
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
@@ -0,0 +1,179 @@
+/*
+ * 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
+
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.TestDispatcherProvider
+import com.android.launcher3.util.rule.setFlags
+import com.android.quickstep.OverviewCommandHelper.CommandInfo
+import com.android.quickstep.OverviewCommandHelper.CommandInfo.CommandStatus
+import com.android.quickstep.OverviewCommandHelper.CommandType
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class OverviewCommandHelperTest {
+ @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+ private lateinit var sut: OverviewCommandHelper
+ private val dispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(dispatcher)
+
+ private var pendingCallbacksWithDelays = mutableListOf<Long>()
+
+ @Suppress("UNCHECKED_CAST")
+ @Before
+ fun setup() {
+ setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT)
+
+ sut =
+ spy(
+ OverviewCommandHelper(
+ touchInteractionService = mock(),
+ overviewComponentObserver = mock(),
+ taskAnimationManager = mock(),
+ dispatcherProvider = TestDispatcherProvider(dispatcher)
+ )
+ )
+
+ doAnswer { invocation ->
+ val pendingCallback = invocation.arguments[1] as () -> Unit
+
+ val delayInMillis = pendingCallbacksWithDelays.removeFirstOrNull()
+ if (delayInMillis != null) {
+ runBlocking {
+ testScope.backgroundScope.launch {
+ delay(delayInMillis)
+ pendingCallback.invoke()
+ }
+ }
+ }
+ delayInMillis == null // if no callback to execute, returns success
+ }
+ .`when`(sut)
+ .executeCommand(any<CommandInfo>(), any())
+ }
+
+ private fun addCallbackDelay(delayInMillis: Long = 0) {
+ pendingCallbacksWithDelays.add(delayInMillis)
+ }
+
+ @Test
+ fun whenFirstCommandIsAdded_executeCommandImmediately() =
+ testScope.runTest {
+ // Add command to queue
+ val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME)!!
+ assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE)
+ runCurrent()
+ assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED)
+ }
+
+ @Test
+ fun whenFirstCommandIsAdded_executeCommandImmediately_WithCallbackDelay() =
+ testScope.runTest {
+ addCallbackDelay(100)
+
+ // Add command to queue
+ val commandType = CommandType.HOME
+ val commandInfo: CommandInfo = sut.addCommand(commandType)!!
+ assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE)
+
+ runCurrent()
+ assertThat(commandInfo.status).isEqualTo(CommandStatus.PROCESSING)
+
+ advanceTimeBy(200L)
+ assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED)
+ }
+
+ @Test
+ fun whenFirstCommandIsPendingCallback_NextCommandWillWait() =
+ testScope.runTest {
+ // Add command to queue
+ addCallbackDelay(100)
+ val commandType1 = CommandType.HOME
+ val commandInfo1: CommandInfo = sut.addCommand(commandType1)!!
+ assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE)
+
+ addCallbackDelay(100)
+ val commandType2 = CommandType.SHOW
+ val commandInfo2: CommandInfo = sut.addCommand(commandType2)!!
+ assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+ runCurrent()
+ assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING)
+ assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+ advanceTimeBy(101L)
+ assertThat(commandInfo1.status).isEqualTo(CommandStatus.COMPLETED)
+ assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING)
+
+ advanceTimeBy(101L)
+ assertThat(commandInfo2.status).isEqualTo(CommandStatus.COMPLETED)
+ }
+
+ @Test
+ fun whenCommandTakesTooLong_TriggerTimeout_AndExecuteNextCommand() =
+ testScope.runTest {
+ // Add command to queue
+ addCallbackDelay(QUEUE_TIMEOUT)
+ val commandType1 = CommandType.HOME
+ val commandInfo1: CommandInfo = sut.addCommand(commandType1)!!
+ assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE)
+
+ addCallbackDelay(100)
+ val commandType2 = CommandType.SHOW
+ val commandInfo2: CommandInfo = sut.addCommand(commandType2)!!
+ assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+ runCurrent()
+ assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING)
+ assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+ advanceTimeBy(QUEUE_TIMEOUT)
+ assertThat(commandInfo1.status).isEqualTo(CommandStatus.CANCELED)
+ assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING)
+
+ advanceTimeBy(101)
+ assertThat(commandInfo2.status).isEqualTo(CommandStatus.COMPLETED)
+ }
+
+ private companion object {
+ const val QUEUE_TIMEOUT = 5001L
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
index 80b9489..c18f604 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -25,6 +25,7 @@
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.quickstep.DeviceConfigWrapper.DEFAULT_LPNH_TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
@@ -39,7 +40,6 @@
import android.os.SystemClock;
import android.view.MotionEvent;
-import android.view.ViewConfiguration;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -142,7 +142,7 @@
@Test
public void testLongPressTriggered() {
mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
@@ -156,7 +156,7 @@
mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
-(TOUCH_SLOP - 1)));
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
@@ -170,7 +170,7 @@
mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
@@ -189,7 +189,7 @@
mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
// We have entered the second stage, so the normal timeout shouldn't trigger.
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -200,7 +200,7 @@
// After an extended time, the long press should trigger.
float extendedDurationMultiplier =
(DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
- SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout()
+ SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
* (extendedDurationMultiplier - 1))); // -1 because we already waited 1x
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -221,7 +221,7 @@
mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
// We have not entered the second stage, so the normal timeout should trigger.
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
@@ -236,7 +236,7 @@
@Test
public void testLongPressAbortedByTouchUp() {
mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -256,7 +256,7 @@
@Test
public void testLongPressAbortedByTouchCancel() {
mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -276,7 +276,7 @@
@Test
public void testLongPressAbortedByTouchSlopPassedVertically() {
mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -297,7 +297,7 @@
@Test
public void testLongPressAbortedByTouchSlopPassedHorizontally() {
mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -326,7 +326,7 @@
mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
-(TOUCH_SLOP - 1)));
// Normal duration shouldn't trigger.
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -338,7 +338,7 @@
// Wait past the extended long press timeout, to be sure it wouldn't have triggered.
float extendedDurationMultiplier =
(DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
- SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout()
+ SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
* (extendedDurationMultiplier - 1))); // -1 because we already waited 1x
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -363,7 +363,7 @@
mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
// Normal duration shouldn't trigger.
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -375,7 +375,7 @@
// Wait past the extended long press timeout, to be sure it wouldn't have triggered.
float extendedDurationMultiplier =
(DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
- SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout()
+ SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
* (extendedDurationMultiplier - 1))); // -1 because we already waited 1x
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -393,7 +393,7 @@
public void testTouchOutsideNavHandleIgnored() {
// Touch the far left side of the screen. (y=0 is top of navbar region, picked arbitrarily)
mUnderTest.onMotionEvent(generateMotionEvent(ACTION_DOWN, 0, 0));
- SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+ SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
// Should be ignored because the x position was not centered in the navbar region.
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
index d2479bc..7c48ea4 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -34,9 +34,6 @@
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED
import com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY
-import com.google.android.apps.nexuslauncher.PrefKey.KEY_ENABLE_MINUS_ONE
-import com.google.android.apps.nexuslauncher.PrefKey.OVERVIEW_SUGGESTED_ACTIONS
-import com.google.android.apps.nexuslauncher.PrefKey.SMARTSPACE_ON_HOME_SCREEN
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -141,7 +138,14 @@
.isTrue()
assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED.id })
.isTrue()
- // LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED
- assertThat(capturedEvents.any { it.id == 617 }).isTrue()
+ assertThat(capturedEvents.any { it.id == LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED }).isTrue()
+ }
+
+ companion object {
+ private const val KEY_ENABLE_MINUS_ONE = "pref_enable_minus_one"
+ private const val OVERVIEW_SUGGESTED_ACTIONS = "pref_overview_action_suggestions"
+ private const val SMARTSPACE_ON_HOME_SCREEN = "pref_smartspace_home_screen"
+
+ private const val LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED = 617
}
}
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index dc8694d..374c07b 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -32,6 +32,7 @@
import android.os.IBinder.DeathRecipient;
import android.os.Message;
import android.os.Messenger;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
@@ -80,8 +81,10 @@
private static final String KEY_SURFACE_PACKAGE = "surface_package";
private static final String KEY_CALLBACK = "callback";
public static final String KEY_HIDE_BOTTOM_ROW = "hide_bottom_row";
+ public static final String KEY_GRID_NAME = "grid_name";
private static final int MESSAGE_ID_UPDATE_PREVIEW = 1337;
+ private static final int MESSAGE_ID_UPDATE_GRID = 7414;
/**
* Here we use the IBinder and the screen ID as the key of the active previews.
@@ -245,11 +248,22 @@
if (destroyed) {
return true;
}
- if (message.what == MESSAGE_ID_UPDATE_PREVIEW) {
- renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW));
- } else {
- destroyObserver(this);
+
+ switch (message.what) {
+ case MESSAGE_ID_UPDATE_PREVIEW:
+ renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW));
+ break;
+ case MESSAGE_ID_UPDATE_GRID:
+ String gridName = message.getData().getString(KEY_GRID_NAME);
+ if (!TextUtils.isEmpty(gridName)) {
+ renderer.updateGrid(gridName);
+ }
+ break;
+ default:
+ destroyObserver(this);
+ break;
}
+
return true;
}
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index addd072..56c4ca4 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -38,6 +38,7 @@
import android.view.SurfaceControlViewHost.SurfacePackage;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -61,6 +62,7 @@
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.Themes;
import com.android.launcher3.widget.LocalColorExtractor;
+import com.android.systemui.shared.Flags;
import java.util.ArrayList;
import java.util.Map;
@@ -96,6 +98,7 @@
private boolean mDestroyed = false;
private LauncherPreviewRenderer mRenderer;
private boolean mHideQsb;
+ @Nullable private FrameLayout mViewRoot = null;
public PreviewSurfaceRenderer(Context context, Bundle bundle) throws Exception {
mContext = context;
@@ -194,6 +197,19 @@
}
/**
+ * Update the grid of the launcher preview
+ *
+ * @param gridName Name of the grid, e.g. normal, practical
+ */
+ public void updateGrid(@NonNull String gridName) {
+ if (gridName.equals(mGridName)) {
+ return;
+ }
+ mGridName = gridName;
+ loadAsync();
+ }
+
+ /**
* Hides the components in the bottom row.
*
* @param hide True to hide and false to show.
@@ -302,11 +318,41 @@
view.setPivotY(0);
view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
- view.setAlpha(0);
- view.animate().alpha(1)
- .setInterpolator(new AccelerateDecelerateInterpolator())
- .setDuration(FADE_IN_ANIMATION_DURATION)
- .start();
- mSurfaceControlViewHost.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight());
+ if (!Flags.newCustomizationPickerUi()) {
+ view.setAlpha(0);
+ view.animate().alpha(1)
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .setDuration(FADE_IN_ANIMATION_DURATION)
+ .start();
+ mSurfaceControlViewHost.setView(
+ view,
+ view.getMeasuredWidth(),
+ view.getMeasuredHeight()
+ );
+ return;
+ }
+
+ if (mViewRoot == null) {
+ mViewRoot = new FrameLayout(inflationContext);
+ FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT, // Width
+ FrameLayout.LayoutParams.WRAP_CONTENT // Height
+ );
+ mViewRoot.setLayoutParams(layoutParams);
+ mViewRoot.addView(view);
+ mViewRoot.setAlpha(0);
+ mViewRoot.animate().alpha(1)
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .setDuration(FADE_IN_ANIMATION_DURATION)
+ .start();
+ mSurfaceControlViewHost.setView(
+ mViewRoot,
+ view.getMeasuredWidth(),
+ view.getMeasuredHeight()
+ );
+ } else {
+ mViewRoot.removeAllViews();
+ mViewRoot.addView(view);
+ }
}
}