Merge "PSS: Task Switcher - Client side actions implementation" into main
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt
index f1cade7..0b19bab 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.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 @@
             }
         }
     }
+
+    /** 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 9938f11..cfbcaf9 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 492d482..4ff54d4e 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 @@
 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 @@
         }
 
     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 6480a47..74d1992 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.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 @@
     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 @@
         }
         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 5bec692..e495466 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 544eb6b..0000000
--- 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 6a535e4..9ef42b4 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 @@
 /** 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 fc5cf7d..eb9e6a5 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 @@
 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 cd1258e..caabc64 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 @@
     /** 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 7840da9..dab7439 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 @@
     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 @@
             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 21aee72..f307761 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 @@
     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 d9754d4..cc8cc51 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 @@
             }
         }
 
+    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 83932b0..dbfab64 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 @@
         )
 
     @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 1c4870b..920e5ee 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 @@
     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 @@
             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 44c411f..28393e8 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.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 @@
 class FakeMediaProjectionManager {
 
     val mediaProjectionManager = mock<MediaProjectionManager>()
+    val helper = mock<MediaProjectionServiceHelper>()
 
     private val callbacks = mutableListOf<MediaProjectionManager.Callback>()
 
@@ -41,6 +44,11 @@
             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 @@
     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 7bd97ce..fdd434a 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.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 @@
 @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 @@
             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 @@
     fun mediaProjectionState_onStop_emitsNotProjecting() =
         testScope.runTest {
             val state by collectLastValue(repo.mediaProjectionState)
-            runCurrent()
 
             fakeMediaProjectionManager.dispatchOnStop()
 
@@ -87,7 +98,6 @@
     fun mediaProjectionState_onSessionSet_sessionNull_emitsNotProjecting() =
         testScope.runTest {
             val state by collectLastValue(repo.mediaProjectionState)
-            runCurrent()
 
             fakeMediaProjectionManager.dispatchOnSessionSet(session = null)
 
@@ -98,7 +108,6 @@
     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 @@
     fun mediaProjectionState_sessionSet_taskWithToken_noMatchingRunningTask_emitsEntireScreen() =
         testScope.runTest {
             val state by collectLastValue(repo.mediaProjectionState)
-            runCurrent()
 
             val taskWindowContainerToken = Binder()
             fakeMediaProjectionManager.dispatchOnSessionSet(
@@ -128,7 +136,6 @@
             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 b2ebe1bc..dfb688b 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 @@
             handler = Handler.getMain(),
             applicationScope = testScope.backgroundScope,
             tasksRepository = tasksRepo,
+            backgroundDispatcher = dispatcher,
+            mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
         )
 
     private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
@@ -118,6 +120,40 @@
         }
 
     @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 d0c6d7c..c4e9393 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.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.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 @@
 @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 @@
             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)
 
-    private val coordinator =
-        TaskSwitcherNotificationCoordinator(
-            context,
-            notificationManager,
-            testScope.backgroundScope,
-            dispatcher,
-            viewModel
-        )
+    private lateinit var coordinator: TaskSwitcherNotificationCoordinator
 
     @Before
     fun setup() {
+        coordinator =
+            TaskSwitcherNotificationCoordinator(
+                context,
+                notificationManager,
+                testScope.backgroundScope,
+                viewModel,
+                fakeBroadcastDispatcher,
+            )
         coordinator.start()
     }
 
@@ -105,7 +111,7 @@
         testScope.runTest {
             fakeMediaProjectionManager.dispatchOnStop()
 
-            verify(notificationManager).cancel(any())
+            verify(notificationManager).cancel(any(), any())
         }
     }
 
@@ -114,7 +120,7 @@
         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 @@
         }
     }
 
+    @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 7d38de4..5dadf21 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 @@
             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 @@
         }
 
     @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)