Merge "[Partial Screen Sharing] Task Switcher Notification" into udc-qpr-dev
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 81012a75..f8c13b0 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1114,6 +1114,16 @@
     <!-- System sharing media projection permission button to continue. [CHAR LIMIT=60] -->
     <string name="media_projection_entry_generic_permission_dialog_continue">Start</string>
 
+    <!-- Task switcher notification -->
+    <!-- Task switcher notification text. [CHAR LIMIT=100] -->
+    <string name="media_projection_task_switcher_text">Sharing pauses when you switch apps</string>
+    <!-- The action for switching to the foreground task. [CHAR LIMIT=40] -->
+    <string name="media_projection_task_switcher_action_switch">Share this app instead</string>
+    <!-- The action for switching back to the projected task. [CHAR LIMIT=40] -->
+    <string name="media_projection_task_switcher_action_back">Switch back</string>
+    <!-- Task switcher notification channel name. [CHAR LIMIT=40] -->
+    <string name="media_projection_task_switcher_notification_channel">App switch</string>
+
     <!-- Title for the dialog that is shown when screen capturing is disabled by enterprise policy. [CHAR LIMIT=100] -->
     <string name="screen_capturing_disabled_by_policy_dialog_title">Blocked by your IT admin</string>
 
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 a4f4076..a437139 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,15 +16,19 @@
 
 package com.android.systemui.mediaprojection.taskswitcher.ui
 
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
 import android.content.Context
 import android.util.Log
-import android.widget.Toast
+import com.android.systemui.R
 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.util.NotificationChannels
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -37,38 +41,69 @@
 @Inject
 constructor(
     private val context: Context,
+    private val notificationManager: NotificationManager,
     @Application private val applicationScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
     private val viewModel: TaskSwitcherNotificationViewModel,
 ) {
-
     fun start() {
         applicationScope.launch {
             viewModel.uiState.flowOn(mainDispatcher).collect { uiState ->
                 Log.d(TAG, "uiState -> $uiState")
                 when (uiState) {
-                    is Showing -> showNotification(uiState)
+                    is Showing -> showNotification()
                     is NotShowing -> hideNotification()
                 }
             }
         }
     }
 
-    private fun showNotification(uiState: Showing) {
-        val text =
-            """
-            Sharing pauses when you switch apps.
-            Share this app instead.
-            Switch back.
-            """
-                .trimIndent()
-        // TODO(b/286201515): Create actual notification.
-        Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
+    private fun showNotification() {
+        notificationManager.notify(TAG, NOTIFICATION_ID, createNotification())
     }
 
-    private fun hideNotification() {}
+    private fun createNotification(): Notification {
+        // TODO(b/286201261): implement actions
+        val actionSwitch =
+            Notification.Action.Builder(
+                    /* icon = */ null,
+                    context.getString(R.string.media_projection_task_switcher_action_switch),
+                    /* intent = */ null
+                )
+                .build()
+
+        val actionBack =
+            Notification.Action.Builder(
+                    /* icon = */ null,
+                    context.getString(R.string.media_projection_task_switcher_action_back),
+                    /* intent = */ null
+                )
+                .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)
+            .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)
+    }
 
     companion object {
         private const val TAG = "TaskSwitchNotifCoord"
+        private const val NOTIFICATION_ID = 5566
     }
 }
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
new file mode 100644
index 0000000..cfbbf76
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.ui
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.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.FakeMediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
+import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() {
+
+    private val notificationManager: NotificationManager = mock()
+
+    private val dispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+    private val fakeActivityTaskManager = FakeActivityTaskManager()
+    private val mediaRepo = FakeMediaProjectionRepository()
+    private val tasksRepo =
+        ActivityTaskManagerTasksRepository(
+            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
+            applicationScope = testScope.backgroundScope,
+            backgroundDispatcher = dispatcher
+        )
+    private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
+    private val viewModel = TaskSwitcherNotificationViewModel(interactor)
+
+    private val coordinator =
+        TaskSwitcherNotificationCoordinator(
+            context,
+            notificationManager,
+            testScope.backgroundScope,
+            dispatcher,
+            viewModel
+        )
+
+    @Before
+    fun setup() {
+        coordinator.start()
+    }
+
+    @Test
+    fun showNotification() {
+        testScope.runTest {
+            switchTask()
+
+            val notification = ArgumentCaptor.forClass(Notification::class.java)
+            verify(notificationManager).notify(any(), any(), notification.capture())
+            assertNotification(notification)
+        }
+    }
+
+    @Test
+    fun hideNotification() {
+        testScope.runTest {
+            mediaRepo.stopProjecting()
+
+            verify(notificationManager).cancel(any())
+        }
+    }
+
+    @Test
+    fun notificationIdIsConsistent() {
+        testScope.runTest {
+            mediaRepo.stopProjecting()
+            val idCancel = argumentCaptor<Int>()
+            verify(notificationManager).cancel(idCancel.capture())
+
+            switchTask()
+            val idNotify = argumentCaptor<Int>()
+            verify(notificationManager).notify(any(), idNotify.capture(), any())
+
+            assertEquals(idCancel.value, idNotify.value)
+        }
+    }
+
+    private fun switchTask() {
+        val projectedTask = FakeActivityTaskManager.createTask(taskId = 1)
+        val foregroundTask = FakeActivityTaskManager.createTask(taskId = 2)
+        mediaRepo.switchProjectedTask(projectedTask)
+        fakeActivityTaskManager.moveTaskToForeground(foregroundTask)
+    }
+
+    private fun assertNotification(notification: ArgumentCaptor<Notification>) {
+        val text = notification.value.extras.getCharSequence(Notification.EXTRA_TEXT)
+        assertEquals(context.getString(R.string.media_projection_task_switcher_text), text)
+
+        val actions = notification.value.actions
+        assertThat(actions).hasLength(2)
+    }
+}