diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 81012a754e357..f8c13b008fd99 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1114,6 +1114,16 @@ Start + + + Sharing pauses when you switch apps + + Share this app instead + + Switch back + + App switch + Blocked by your IT admin 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 a4f407612fa86..a437139226de4 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 @@ 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 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 0000000000000..cfbbf768e1fe8 --- /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() + verify(notificationManager).cancel(idCancel.capture()) + + switchTask() + val idNotify = argumentCaptor() + 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) { + 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) + } +}