Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e08d6831 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Adds a timeout for OverviewCommandHelper commands (3/3)" into main

parents 987002bc ad3c422f
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -39,3 +39,12 @@ flag {
    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
+112 −34
Original line number Diff line number Diff line
@@ -25,16 +25,26 @@ import android.util.Log
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.recents.model.ThumbnailData
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 @@ class OverviewCommandHelper(
     * 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 @@ class OverviewCommandHelper(
        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 @@ class OverviewCommandHelper(
     * completion (returns false).
     */
    @UiThread
    private fun executeNext() {
    private fun processNextCommand() {
        val command: CommandInfo =
            commandQueue.firstOrNull()
                ?: run {
@@ -119,7 +150,16 @@ class OverviewCommandHelper(
        command.status = CommandStatus.PROCESSING
        Log.d(TAG, "executing command: $command")

        val result = executeCommand(command)
        if (enableOverviewCommandHelperTimeout()) {
            coroutineScope.launch(dispatcherProvider.main) {
                withTimeout(QUEUE_WAIT_DURATION_IN_MS) {
                    executeCommandSuspended(command)
                    ensureActive()
                    onCommandFinished(command)
                }
            }
        } else {
            val result = executeCommand(command, onCallbackResult = { onCommandFinished(command) })
            Log.d(TAG, "command executed: $command with result: $result")
            if (result) {
                onCommandFinished(command)
@@ -127,12 +167,15 @@ class OverviewCommandHelper(
                Log.d(TAG, "waiting for command callback: $command")
            }
        }
    }

    /**
     * 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 @@ class OverviewCommandHelper(
        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 @@ class OverviewCommandHelper(
                    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 @@ class OverviewCommandHelper(
                    } else {
                        recentsView.nextTaskView ?: recentsView.runningTaskView
                    }
                launchTask(recentsView, taskView, command)
                launchTask(recentsView, taskView, command, onCallbackResult)
            }
            HOME -> {
                recentsView.startHome()
@@ -182,7 +247,8 @@ class OverviewCommandHelper(
    private fun launchTask(
        recents: RecentsView<*, *>,
        taskView: TaskView?,
        command: CommandInfo
        command: CommandInfo,
        onCallbackResult: () -> Unit
    ): Boolean {
        var callbackList: RunnableList? = null
        if (taskView != null) {
@@ -194,7 +260,7 @@ class OverviewCommandHelper(
        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 @@ class OverviewCommandHelper(
        }
    }

    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 @@ class OverviewCommandHelper(
                    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 @@ class OverviewCommandHelper(
                command.createTime
            )
        interactionHandler.setGestureEndCallback {
            onTransitionComplete(command, interactionHandler)
            onTransitionComplete(command, interactionHandler, onCallbackResult)
        }
        interactionHandler.initWhenReady("OverviewCommandHelper: command.type=${command.type}")

@@ -321,11 +390,6 @@ class OverviewCommandHelper(
                }
            }

        // 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,12 +415,16 @@ class OverviewCommandHelper(
        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. */
@@ -373,7 +441,14 @@ class OverviewCommandHelper(

        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 @@ class OverviewCommandHelper(
        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 @@ class OverviewCommandHelper(
        enum class CommandStatus {
            IDLE,
            PROCESSING,
            COMPLETED
            COMPLETED,
            CANCELED
        }
    }

@@ -489,5 +566,6 @@ class OverviewCommandHelper(
         * 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
    }
}
+179 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}