diff options
19 files changed, 357 insertions, 106 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt index f1cade7512e2..0b19bab5c7c5 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt @@ -24,12 +24,14 @@ import android.media.projection.ReviewGrantedConsentResult import android.os.RemoteException import android.os.ServiceManager import android.util.Log +import android.window.WindowContainerToken +import javax.inject.Inject /** * Helper class that handles the media projection service related actions. It simplifies invoking * the MediaProjectionManagerService and updating the permission consent. */ -class MediaProjectionServiceHelper { +class MediaProjectionServiceHelper @Inject constructor() { companion object { private const val TAG = "MediaProjectionServiceHelper" private val service = @@ -90,4 +92,16 @@ class MediaProjectionServiceHelper { } } } + + /** Updates the projected task to the task that has a matching [WindowContainerToken]. */ + fun updateTaskRecordingSession(token: WindowContainerToken): Boolean { + return try { + true + // TODO: actually call the service once it is implemented + // service.updateTaskRecordingSession(token) + } catch (e: RemoteException) { + Log.e(TAG, "Unable to updateTaskRecordingSession", e) + false + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt index 9938f11e5d4c..cfbcaf91b791 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt @@ -16,11 +16,11 @@ package com.android.systemui.mediaprojection.taskswitcher.data.model -import android.app.TaskInfo +import android.app.ActivityManager.RunningTaskInfo /** Represents the state of media projection. */ sealed interface MediaProjectionState { object NotProjecting : MediaProjectionState object EntireScreen : MediaProjectionState - data class SingleTask(val task: TaskInfo) : MediaProjectionState + data class SingleTask(val task: RunningTaskInfo) : MediaProjectionState } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt index 492d482459d6..4ff54d4eae65 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt @@ -17,10 +17,14 @@ package com.android.systemui.mediaprojection.taskswitcher.data.repository import android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityOptions +import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED import android.app.ActivityTaskManager +import android.app.IActivityTaskManager import android.app.TaskStackListener import android.os.IBinder import android.util.Log +import android.view.Display import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -40,11 +44,24 @@ import kotlinx.coroutines.withContext class ActivityTaskManagerTasksRepository @Inject constructor( - private val activityTaskManager: ActivityTaskManager, + private val activityTaskManager: IActivityTaskManager, @Application private val applicationScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, ) : TasksRepository { + override suspend fun launchRecentTask(taskInfo: RunningTaskInfo) { + withContext(backgroundDispatcher) { + val activityOptions = ActivityOptions.makeBasic() + activityOptions.pendingIntentBackgroundActivityStartMode = + MODE_BACKGROUND_ACTIVITY_START_ALLOWED + activityOptions.launchDisplayId = taskInfo.displayId + activityTaskManager.startActivityFromRecents( + taskInfo.taskId, + activityOptions.toBundle() + ) + } + } + override suspend fun findRunningTaskFromWindowContainerToken( windowContainerToken: IBinder ): RunningTaskInfo? = @@ -53,7 +70,14 @@ constructor( } private suspend fun getRunningTasks(): List<RunningTaskInfo> = - withContext(backgroundDispatcher) { activityTaskManager.getTasks(Integer.MAX_VALUE) } + withContext(backgroundDispatcher) { + activityTaskManager.getTasks( + /* maxNum = */ Integer.MAX_VALUE, + /* filterForVisibleRecents = */ false, + /* keepIntentExtra = */ false, + /* displayId = */ Display.INVALID_DISPLAY + ) + } override val foregroundTask: Flow<RunningTaskInfo> = conflatedCallbackFlow { diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt index 6480a47e8ea2..74d19921c706 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt @@ -16,6 +16,7 @@ package com.android.systemui.mediaprojection.taskswitcher.data.repository +import android.app.ActivityManager.RunningTaskInfo import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager import android.os.Handler @@ -26,15 +27,19 @@ import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLoggin import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.mediaprojection.MediaProjectionServiceHelper import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @SysUISingleton class MediaProjectionManagerRepository @@ -43,9 +48,21 @@ constructor( private val mediaProjectionManager: MediaProjectionManager, @Main private val handler: Handler, @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, private val tasksRepository: TasksRepository, + private val mediaProjectionServiceHelper: MediaProjectionServiceHelper, ) : MediaProjectionRepository { + override suspend fun switchProjectedTask(task: RunningTaskInfo) { + withContext(backgroundDispatcher) { + if (mediaProjectionServiceHelper.updateTaskRecordingSession(task.token)) { + Log.d(TAG, "Successfully switched projected task") + } else { + Log.d(TAG, "Failed to switch projected task") + } + } + } + override val mediaProjectionState: Flow<MediaProjectionState> = conflatedCallbackFlow { val callback = @@ -82,7 +99,9 @@ constructor( } val matchingTask = tasksRepository.findRunningTaskFromWindowContainerToken( - checkNotNull(session.tokenToRecord)) ?: return MediaProjectionState.EntireScreen + checkNotNull(session.tokenToRecord) + ) + ?: return MediaProjectionState.EntireScreen return MediaProjectionState.SingleTask(matchingTask) } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt index 5bec6925babe..e495466008ce 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt @@ -16,12 +16,16 @@ package com.android.systemui.mediaprojection.taskswitcher.data.repository +import android.app.ActivityManager.RunningTaskInfo import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState import kotlinx.coroutines.flow.Flow /** Represents a repository to retrieve and change data related to media projection. */ interface MediaProjectionRepository { + /** Switches the task that should be projected. */ + suspend fun switchProjectedTask(task: RunningTaskInfo) + /** Represents the current [MediaProjectionState]. */ val mediaProjectionState: Flow<MediaProjectionState> } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/NoOpMediaProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/NoOpMediaProjectionRepository.kt deleted file mode 100644 index 544eb6b99d4f..000000000000 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/NoOpMediaProjectionRepository.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2023 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.systemui.mediaprojection.taskswitcher.data.repository - -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow - -/** - * No-op implementation of [MediaProjectionRepository] that does nothing. Currently used as a - * placeholder, while the real implementation is not completed. - */ -@SysUISingleton -class NoOpMediaProjectionRepository @Inject constructor() : MediaProjectionRepository { - - override val mediaProjectionState: Flow<MediaProjectionState> = emptyFlow() -} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/TasksRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/TasksRepository.kt index 6a535e4ecc50..9ef42b4de45c 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/TasksRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/TasksRepository.kt @@ -23,6 +23,8 @@ import kotlinx.coroutines.flow.Flow /** Repository responsible for retrieving data related to running tasks. */ interface TasksRepository { + suspend fun launchRecentTask(taskInfo: RunningTaskInfo) + /** * Tries to find a [RunningTaskInfo] with a matching window container token. Returns `null` when * no matching task was found. diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt index fc5cf7d75bdf..eb9e6a5de057 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.mediaprojection.taskswitcher.domain.interactor +import android.app.ActivityManager.RunningTaskInfo import android.app.TaskInfo import android.content.Intent import android.util.Log @@ -37,10 +38,18 @@ import kotlinx.coroutines.flow.map class TaskSwitchInteractor @Inject constructor( - mediaProjectionRepository: MediaProjectionRepository, + private val mediaProjectionRepository: MediaProjectionRepository, private val tasksRepository: TasksRepository, ) { + suspend fun switchProjectedTask(task: RunningTaskInfo) { + mediaProjectionRepository.switchProjectedTask(task) + } + + suspend fun goBackToTask(task: RunningTaskInfo) { + tasksRepository.launchRecentTask(task) + } + /** * Emits a stream of changes to the state of task switching, in the context of media projection. */ diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/model/TaskSwitchState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/model/TaskSwitchState.kt index cd1258ed6aa8..caabc64efae1 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/model/TaskSwitchState.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/model/TaskSwitchState.kt @@ -16,7 +16,7 @@ package com.android.systemui.mediaprojection.taskswitcher.domain.model -import android.app.TaskInfo +import android.app.ActivityManager.RunningTaskInfo /** Represents tha state of task switching in the context of single task media projection. */ sealed interface TaskSwitchState { @@ -25,6 +25,8 @@ sealed interface TaskSwitchState { /** The foreground task is the same as the task that is currently being projected. */ object TaskUnchanged : TaskSwitchState /** The foreground task is a different one to the task it currently being projected. */ - data class TaskSwitched(val projectedTask: TaskInfo, val foregroundTask: TaskInfo) : - TaskSwitchState + data class TaskSwitched( + val projectedTask: RunningTaskInfo, + val foregroundTask: RunningTaskInfo + ) : TaskSwitchState } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt index 7840da960a83..dab7439f0f0c 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt @@ -16,23 +16,25 @@ package com.android.systemui.mediaprojection.taskswitcher.ui +import android.app.ActivityManager.RunningTaskInfo import android.app.Notification -import android.app.NotificationChannel import android.app.NotificationManager +import android.app.PendingIntent import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Parcelable import android.util.Log -import com.android.systemui.res.R +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.NotShowing import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.Showing import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel +import com.android.systemui.res.R import com.android.systemui.util.NotificationChannels import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch /** Coordinator responsible for showing/hiding the task switcher notification. */ @@ -43,32 +45,54 @@ constructor( private val context: Context, private val notificationManager: NotificationManager, @Application private val applicationScope: CoroutineScope, - @Main private val mainDispatcher: CoroutineDispatcher, private val viewModel: TaskSwitcherNotificationViewModel, + private val broadcastDispatcher: BroadcastDispatcher, ) { + fun start() { applicationScope.launch { - viewModel.uiState.flowOn(mainDispatcher).collect { uiState -> - Log.d(TAG, "uiState -> $uiState") - when (uiState) { - is Showing -> showNotification() - is NotShowing -> hideNotification() + launch { + viewModel.uiState.collect { uiState -> + Log.d(TAG, "uiState -> $uiState") + when (uiState) { + is Showing -> showNotification(uiState) + is NotShowing -> hideNotification() + } } } + launch { + broadcastDispatcher + .broadcastFlow(IntentFilter(SWITCH_ACTION)) { intent, _ -> + intent.requireParcelableExtra<RunningTaskInfo>(EXTRA_ACTION_TASK) + } + .collect { task: RunningTaskInfo -> + Log.d(TAG, "Switch action triggered: $task") + viewModel.onSwitchTaskClicked(task) + } + } + launch { + broadcastDispatcher + .broadcastFlow(IntentFilter(GO_BACK_ACTION)) { intent, _ -> + intent.requireParcelableExtra<RunningTaskInfo>(EXTRA_ACTION_TASK) + } + .collect { task -> + Log.d(TAG, "Go back action triggered: $task") + viewModel.onGoBackToTaskClicked(task) + } + } } } - private fun showNotification() { - notificationManager.notify(TAG, NOTIFICATION_ID, createNotification()) + private fun showNotification(uiState: Showing) { + notificationManager.notify(TAG, NOTIFICATION_ID, createNotification(uiState)) } - private fun createNotification(): Notification { - // TODO(b/286201261): implement actions + private fun createNotification(uiState: Showing): Notification { val actionSwitch = Notification.Action.Builder( /* icon = */ null, context.getString(R.string.media_projection_task_switcher_action_switch), - /* intent = */ null + createActionPendingIntent(action = SWITCH_ACTION, task = uiState.foregroundTask) ) .build() @@ -76,34 +100,40 @@ constructor( Notification.Action.Builder( /* icon = */ null, context.getString(R.string.media_projection_task_switcher_action_back), - /* intent = */ null + createActionPendingIntent(action = GO_BACK_ACTION, task = uiState.projectedTask) ) .build() - - val channel = - NotificationChannel( - NotificationChannels.HINTS, - context.getString(R.string.media_projection_task_switcher_notification_channel), - NotificationManager.IMPORTANCE_HIGH - ) - notificationManager.createNotificationChannel(channel) - return Notification.Builder(context, channel.id) + return Notification.Builder(context, NotificationChannels.ALERTS) .setSmallIcon(R.drawable.qs_screen_record_icon_on) .setAutoCancel(true) .setContentText(context.getString(R.string.media_projection_task_switcher_text)) .addAction(actionSwitch) .addAction(actionBack) - .setPriority(Notification.PRIORITY_HIGH) - .setDefaults(Notification.DEFAULT_VIBRATE) .build() } private fun hideNotification() { - notificationManager.cancel(NOTIFICATION_ID) + notificationManager.cancel(TAG, NOTIFICATION_ID) } + private fun createActionPendingIntent(action: String, task: RunningTaskInfo) = + PendingIntent.getBroadcast( + context, + /* requestCode= */ 0, + Intent(action).apply { putExtra(EXTRA_ACTION_TASK, task) }, + /* flags= */ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + companion object { private const val TAG = "TaskSwitchNotifCoord" private const val NOTIFICATION_ID = 5566 + + private const val EXTRA_ACTION_TASK = "extra_task" + + private const val SWITCH_ACTION = "com.android.systemui.mediaprojection.SWITCH_TASK" + private const val GO_BACK_ACTION = "com.android.systemui.mediaprojection.GO_BACK" } } + +private fun <T : Parcelable> Intent.requireParcelableExtra(key: String) = + getParcelableExtra<T>(key)!! diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/model/TaskSwitcherNotificationUiState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/model/TaskSwitcherNotificationUiState.kt index 21aee72d17ae..f307761a1875 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/model/TaskSwitcherNotificationUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/model/TaskSwitcherNotificationUiState.kt @@ -16,7 +16,7 @@ package com.android.systemui.mediaprojection.taskswitcher.ui.model -import android.app.TaskInfo +import android.app.ActivityManager.RunningTaskInfo /** Represents the UI state for the task switcher notification. */ sealed interface TaskSwitcherNotificationUiState { @@ -24,7 +24,7 @@ sealed interface TaskSwitcherNotificationUiState { object NotShowing : TaskSwitcherNotificationUiState /** The notification should be shown. */ data class Showing( - val projectedTask: TaskInfo, - val foregroundTask: TaskInfo, + val projectedTask: RunningTaskInfo, + val foregroundTask: RunningTaskInfo, ) : TaskSwitcherNotificationUiState } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt index d9754d4429d4..cc8cc5165e4f 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt @@ -16,15 +16,24 @@ package com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel +import android.app.ActivityManager.RunningTaskInfo import android.util.Log +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext -class TaskSwitcherNotificationViewModel @Inject constructor(interactor: TaskSwitchInteractor) { +class TaskSwitcherNotificationViewModel +@Inject +constructor( + private val interactor: TaskSwitchInteractor, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { val uiState: Flow<TaskSwitcherNotificationUiState> = interactor.taskSwitchChanges.map { taskSwitchChange -> @@ -43,6 +52,13 @@ class TaskSwitcherNotificationViewModel @Inject constructor(interactor: TaskSwit } } + suspend fun onSwitchTaskClicked(task: RunningTaskInfo) { + interactor.switchProjectedTask(task) + } + + suspend fun onGoBackToTaskClicked(task: RunningTaskInfo) = + withContext(backgroundDispatcher) { interactor.goBackToTask(task) } + companion object { private const val TAG = "TaskSwitchNotifVM" } diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt index 83932b0a6133..dbfab64b004a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt @@ -49,6 +49,19 @@ class ActivityTaskManagerTasksRepositoryTest : SysuiTestCase() { ) @Test + fun launchRecentTask_taskIsMovedToForeground() = + testScope.runTest { + val currentForegroundTask by collectLastValue(repo.foregroundTask) + val newForegroundTask = createTask(taskId = 1) + val backgroundTask = createTask(taskId = 2) + fakeActivityTaskManager.addRunningTasks(backgroundTask, newForegroundTask) + + repo.launchRecentTask(newForegroundTask) + + assertThat(currentForegroundTask).isEqualTo(newForegroundTask) + } + + @Test fun findRunningTaskFromWindowContainerToken_noMatch_returnsNull() { fakeActivityTaskManager.addRunningTasks(createTask(taskId = 1), createTask(taskId = 2)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt index 1c4870bc32b1..920e5ee94cca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt @@ -17,7 +17,7 @@ package com.android.systemui.mediaprojection.taskswitcher.data.repository import android.app.ActivityManager.RunningTaskInfo -import android.app.ActivityTaskManager +import android.app.IActivityTaskManager import android.app.TaskStackListener import android.content.Intent import android.window.IWindowContainerToken @@ -31,7 +31,7 @@ class FakeActivityTaskManager { private val runningTasks = mutableListOf<RunningTaskInfo>() private val taskTaskListeners = mutableListOf<TaskStackListener>() - val activityTaskManager = mock<ActivityTaskManager>() + val activityTaskManager = mock<IActivityTaskManager>() init { whenever(activityTaskManager.registerTaskStackListener(any())).thenAnswer { @@ -42,10 +42,20 @@ class FakeActivityTaskManager { taskTaskListeners -= it.arguments[0] as TaskStackListener return@thenAnswer Unit } - whenever(activityTaskManager.getTasks(any())).thenAnswer { + whenever(activityTaskManager.getTasks(any(), any(), any(), any())).thenAnswer { val maxNumTasks = it.arguments[0] as Int return@thenAnswer runningTasks.take(maxNumTasks) } + whenever(activityTaskManager.startActivityFromRecents(any(), any())).thenAnswer { + val taskId = it.arguments[0] as Int + val runningTask = runningTasks.find { runningTask -> runningTask.taskId == taskId } + if (runningTask != null) { + moveTaskToForeground(runningTask) + return@thenAnswer 0 + } else { + return@thenAnswer -1 + } + } } fun moveTaskToForeground(task: RunningTaskInfo) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt index 44c411fdb1d1..28393e837b93 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt @@ -22,6 +22,8 @@ import android.os.Binder import android.os.IBinder import android.os.UserHandle import android.view.ContentRecordingSession +import android.window.WindowContainerToken +import com.android.systemui.mediaprojection.MediaProjectionServiceHelper import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -29,6 +31,7 @@ import com.android.systemui.util.mockito.whenever class FakeMediaProjectionManager { val mediaProjectionManager = mock<MediaProjectionManager>() + val helper = mock<MediaProjectionServiceHelper>() private val callbacks = mutableListOf<MediaProjectionManager.Callback>() @@ -41,6 +44,11 @@ class FakeMediaProjectionManager { callbacks -= it.arguments[0] as MediaProjectionManager.Callback return@thenAnswer Unit } + whenever(helper.updateTaskRecordingSession(any())).thenAnswer { + val token = it.arguments[0] as WindowContainerToken + dispatchOnSessionSet(session = createSingleTaskSession(token.asBinder())) + return@thenAnswer true + } } fun dispatchOnStart(info: MediaProjectionInfo = DEFAULT_INFO) { @@ -61,6 +69,7 @@ class FakeMediaProjectionManager { companion object { fun createDisplaySession(): ContentRecordingSession = ContentRecordingSession.createDisplaySession(/* displayToMirror = */ 123) + fun createSingleTaskSession(token: IBinder = Binder()): ContentRecordingSession = ContentRecordingSession.createTaskSession(token) diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt index 7bd97ce2670c..fdd434acdc9f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt @@ -28,9 +28,8 @@ import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeAct import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createToken import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -40,7 +39,7 @@ import org.junit.runner.RunWith @SmallTest class MediaProjectionManagerRepositoryTest : SysuiTestCase() { - private val dispatcher = StandardTestDispatcher() + private val dispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(dispatcher) private val fakeMediaProjectionManager = FakeMediaProjectionManager() @@ -58,14 +57,27 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, handler = Handler.getMain(), applicationScope = testScope.backgroundScope, - tasksRepository = tasksRepo + tasksRepository = tasksRepo, + backgroundDispatcher = dispatcher, + mediaProjectionServiceHelper = fakeMediaProjectionManager.helper ) @Test + fun switchProjectedTask_stateIsUpdatedWithNewTask() = + testScope.runTest { + val task = createTask(taskId = 1) + val state by collectLastValue(repo.mediaProjectionState) + + fakeActivityTaskManager.addRunningTasks(task) + repo.switchProjectedTask(task) + + assertThat(state).isEqualTo(MediaProjectionState.SingleTask(task)) + } + + @Test fun mediaProjectionState_onStart_emitsNotProjecting() = testScope.runTest { val state by collectLastValue(repo.mediaProjectionState) - runCurrent() fakeMediaProjectionManager.dispatchOnStart() @@ -76,7 +88,6 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { fun mediaProjectionState_onStop_emitsNotProjecting() = testScope.runTest { val state by collectLastValue(repo.mediaProjectionState) - runCurrent() fakeMediaProjectionManager.dispatchOnStop() @@ -87,7 +98,6 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { fun mediaProjectionState_onSessionSet_sessionNull_emitsNotProjecting() = testScope.runTest { val state by collectLastValue(repo.mediaProjectionState) - runCurrent() fakeMediaProjectionManager.dispatchOnSessionSet(session = null) @@ -98,7 +108,6 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { fun mediaProjectionState_onSessionSet_contentToRecordDisplay_emitsEntireScreen() = testScope.runTest { val state by collectLastValue(repo.mediaProjectionState) - runCurrent() fakeMediaProjectionManager.dispatchOnSessionSet( session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123) @@ -111,7 +120,6 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { fun mediaProjectionState_sessionSet_taskWithToken_noMatchingRunningTask_emitsEntireScreen() = testScope.runTest { val state by collectLastValue(repo.mediaProjectionState) - runCurrent() val taskWindowContainerToken = Binder() fakeMediaProjectionManager.dispatchOnSessionSet( @@ -128,7 +136,6 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { val task = createTask(taskId = 1, token = token) fakeActivityTaskManager.addRunningTasks(task) val state by collectLastValue(repo.mediaProjectionState) - runCurrent() fakeMediaProjectionManager.dispatchOnSessionSet( session = ContentRecordingSession.createTaskSession(token.asBinder()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt index b2ebe1bcbc8b..dfb688bbde4b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt @@ -61,6 +61,8 @@ class TaskSwitchInteractorTest : SysuiTestCase() { handler = Handler.getMain(), applicationScope = testScope.backgroundScope, tasksRepository = tasksRepo, + backgroundDispatcher = dispatcher, + mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, ) private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo) @@ -118,6 +120,40 @@ class TaskSwitchInteractorTest : SysuiTestCase() { } @Test + fun taskSwitchChanges_projectingTask_foregroundTaskDifferent_thenSwitched_emitsUnchanged() = + testScope.runTest { + val projectedTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 1) + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(token = projectedTask.token.asBinder()) + ) + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + interactor.switchProjectedTask(foregroundTask) + + assertThat(taskSwitchState).isEqualTo(TaskSwitchState.TaskUnchanged) + } + + @Test + fun taskSwitchChanges_projectingTask_foregroundTaskDifferent_thenWentBack_emitsUnchanged() = + testScope.runTest { + val projectedTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 1) + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(token = projectedTask.token.asBinder()) + ) + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + interactor.goBackToTask(projectedTask) + + assertThat(taskSwitchState).isEqualTo(TaskSwitchState.TaskUnchanged) + } + + @Test fun taskSwitchChanges_projectingTask_foregroundTaskLauncher_emitsTaskUnchanged() = testScope.runTest { val projectedTask = createTask(taskId = 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt index d0c6d7cddc46..c4e939339fa1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt @@ -21,14 +21,15 @@ import android.app.NotificationManager import android.os.Handler import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel +import com.android.systemui.res.R import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock @@ -42,6 +43,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.Mockito.never import org.mockito.Mockito.verify @OptIn(ExperimentalCoroutinesApi::class) @@ -49,7 +51,7 @@ import org.mockito.Mockito.verify @SmallTest class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() { - private val notificationManager: NotificationManager = mock() + private val notificationManager = mock<NotificationManager>() private val dispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(dispatcher) @@ -70,22 +72,26 @@ class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() { handler = Handler.getMain(), applicationScope = testScope.backgroundScope, tasksRepository = tasksRepo, + backgroundDispatcher = dispatcher, + mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, ) private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo) - private val viewModel = TaskSwitcherNotificationViewModel(interactor) - - private val coordinator = - TaskSwitcherNotificationCoordinator( - context, - notificationManager, - testScope.backgroundScope, - dispatcher, - viewModel - ) + private val viewModel = + TaskSwitcherNotificationViewModel(interactor, backgroundDispatcher = dispatcher) + + private lateinit var coordinator: TaskSwitcherNotificationCoordinator @Before fun setup() { + coordinator = + TaskSwitcherNotificationCoordinator( + context, + notificationManager, + testScope.backgroundScope, + viewModel, + fakeBroadcastDispatcher, + ) coordinator.start() } @@ -105,7 +111,7 @@ class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() { testScope.runTest { fakeMediaProjectionManager.dispatchOnStop() - verify(notificationManager).cancel(any()) + verify(notificationManager).cancel(any(), any()) } } @@ -114,7 +120,7 @@ class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() { testScope.runTest { fakeMediaProjectionManager.dispatchOnStop() val idCancel = argumentCaptor<Int>() - verify(notificationManager).cancel(idCancel.capture()) + verify(notificationManager).cancel(any(), idCancel.capture()) switchTask() val idNotify = argumentCaptor<Int>() @@ -124,9 +130,55 @@ class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() { } } + @Test + fun switchTaskAction_hidesNotification() = + testScope.runTest { + switchTask() + val notification = argumentCaptor<Notification>() + verify(notificationManager).notify(any(), any(), notification.capture()) + verify(notificationManager, never()).cancel(any(), any()) + + val action = findSwitchAction(notification.value) + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + action.actionIntent.intent + ) + + verify(notificationManager).cancel(any(), any()) + } + + @Test + fun goBackAction_hidesNotification() = + testScope.runTest { + switchTask() + val notification = argumentCaptor<Notification>() + verify(notificationManager).notify(any(), any(), notification.capture()) + verify(notificationManager, never()).cancel(any(), any()) + + val action = findGoBackAction(notification.value) + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + action.actionIntent.intent + ) + + verify(notificationManager).cancel(any(), any()) + } + + private fun findSwitchAction(notification: Notification): Notification.Action { + return notification.actions.first { + it.title == context.getString(R.string.media_projection_task_switcher_action_switch) + } + } + + private fun findGoBackAction(notification: Notification): Notification.Action { + return notification.actions.first { + it.title == context.getString(R.string.media_projection_task_switcher_action_back) + } + } + private fun switchTask() { - val projectedTask = FakeActivityTaskManager.createTask(taskId = 1) - val foregroundTask = FakeActivityTaskManager.createTask(taskId = 2) + val projectedTask = createTask(taskId = 1) + val foregroundTask = createTask(taskId = 2) fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) fakeMediaProjectionManager.dispatchOnSessionSet( session = diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt index 7d38de4bc2d3..5dadf21a46b9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt @@ -63,11 +63,14 @@ class TaskSwitcherNotificationViewModelTest : SysuiTestCase() { handler = Handler.getMain(), applicationScope = testScope.backgroundScope, tasksRepository = tasksRepo, + backgroundDispatcher = dispatcher, + mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, ) private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo) - private val viewModel = TaskSwitcherNotificationViewModel(interactor) + private val viewModel = + TaskSwitcherNotificationViewModel(interactor, backgroundDispatcher = dispatcher) @Test fun uiState_notProjecting_emitsNotShowing() = @@ -135,6 +138,40 @@ class TaskSwitcherNotificationViewModelTest : SysuiTestCase() { } @Test + fun uiState_projectingTask_foregroundTaskChanged_thenTaskSwitched_emitsNotShowing() = + testScope.runTest { + val projectedTask = createTask(taskId = 1) + val foregroundTask = createTask(taskId = 2) + val uiState by collectLastValue(viewModel.uiState) + + fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(projectedTask.token.asBinder()) + ) + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + viewModel.onSwitchTaskClicked(foregroundTask) + + assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) + } + + @Test + fun uiState_projectingTask_foregroundTaskChanged_thenGoBack_emitsNotShowing() = + testScope.runTest { + val projectedTask = createTask(taskId = 1) + val foregroundTask = createTask(taskId = 2) + val uiState by collectLastValue(viewModel.uiState) + + fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(projectedTask.token.asBinder()) + ) + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + viewModel.onGoBackToTaskClicked(projectedTask) + + assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) + } + + @Test fun uiState_projectingTask_foregroundTaskChanged_same_emitsNotShowing() = testScope.runTest { val projectedTask = createTask(taskId = 1) |