Loading packages/SystemUI/res/values/strings.xml +10 −0 Original line number Diff line number Diff line Loading @@ -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> Loading packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt +49 −14 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -37,38 +41,69 @@ class TaskSwitcherNotificationCoordinator @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 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() {} private fun hideNotification() { notificationManager.cancel(NOTIFICATION_ID) } companion object { private const val TAG = "TaskSwitchNotifCoord" private const val NOTIFICATION_ID = 5566 } } packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt 0 → 100644 +128 −0 Original line number Diff line number Diff line /* * 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) } } Loading
packages/SystemUI/res/values/strings.xml +10 −0 Original line number Diff line number Diff line Loading @@ -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> Loading
packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt +49 −14 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -37,38 +41,69 @@ class TaskSwitcherNotificationCoordinator @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 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() {} private fun hideNotification() { notificationManager.cancel(NOTIFICATION_ID) } companion object { private const val TAG = "TaskSwitchNotifCoord" private const val NOTIFICATION_ID = 5566 } }
packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt 0 → 100644 +128 −0 Original line number Diff line number Diff line /* * 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) } }