From 27f8a2266ca703f4930dc1d6b825a451c0eef5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6llner?= Date: Wed, 21 Jun 2023 17:16:50 +0200 Subject: [PATCH] Partial Screen Sharing: Task Switcher - UI Layer implementation - Defines model/state - Implements view model - Implements "empty" coordinator, that will later be filled with the code to show the actual notification Bug: 286201261 Test: TaskSwitcherNotificationViewModelTest.kt Change-Id: I3aae76de69e1acedc9eeb0d684c698d9d5dbce66 --- .../ui/TaskSwitcherNotificationCoordinator.kt | 74 +++++++++ .../model/TaskSwitcherNotificationUiState.kt | 30 ++++ .../TaskSwitcherNotificationViewModel.kt | 49 ++++++ .../TaskSwitcherNotificationViewModelTest.kt | 141 ++++++++++++++++++ 4 files changed, 294 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt create mode 100644 packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/model/TaskSwitcherNotificationUiState.kt create mode 100644 packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt 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 new file mode 100644 index 0000000000000..a4f407612fa86 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt @@ -0,0 +1,74 @@ +/* + * 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.content.Context +import android.util.Log +import android.widget.Toast +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 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. */ +@SysUISingleton +class TaskSwitcherNotificationCoordinator +@Inject +constructor( + private val context: Context, + @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 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 hideNotification() {} + + companion object { + private const val TAG = "TaskSwitchNotifCoord" + } +} 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 new file mode 100644 index 0000000000000..21aee72d17ae4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/model/TaskSwitcherNotificationUiState.kt @@ -0,0 +1,30 @@ +/* + * 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.model + +import android.app.TaskInfo + +/** Represents the UI state for the task switcher notification. */ +sealed interface TaskSwitcherNotificationUiState { + /** The notification should not be shown. */ + object NotShowing : TaskSwitcherNotificationUiState + /** The notification should be shown. */ + data class Showing( + val projectedTask: TaskInfo, + val foregroundTask: TaskInfo, + ) : 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 new file mode 100644 index 0000000000000..d9754d4429d47 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt @@ -0,0 +1,49 @@ +/* + * 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.viewmodel + +import android.util.Log +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.flow.Flow +import kotlinx.coroutines.flow.map + +class TaskSwitcherNotificationViewModel @Inject constructor(interactor: TaskSwitchInteractor) { + + val uiState: Flow = + interactor.taskSwitchChanges.map { taskSwitchChange -> + Log.d(TAG, "taskSwitchChange: $taskSwitchChange") + when (taskSwitchChange) { + is TaskSwitchState.TaskSwitched -> { + TaskSwitcherNotificationUiState.Showing( + projectedTask = taskSwitchChange.projectedTask, + foregroundTask = taskSwitchChange.foregroundTask, + ) + } + is TaskSwitchState.NotProjectingTask, + is TaskSwitchState.TaskUnchanged -> { + TaskSwitcherNotificationUiState.NotShowing + } + } + } + + companion object { + private const val TAG = "TaskSwitchNotifVM" + } +} 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 new file mode 100644 index 0000000000000..ea44fb3b1f6ec --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt @@ -0,0 +1,141 @@ +/* + * 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.viewmodel + +import android.content.Intent +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +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.FakeMediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor +import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +@SmallTest +class TaskSwitcherNotificationViewModelTest : SysuiTestCase() { + + 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) + + @Test + fun uiState_notProjecting_emitsNotShowing() = + testScope.runTest { + mediaRepo.stopProjecting() + val uiState by collectLastValue(viewModel.uiState) + + assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) + } + + @Test + fun uiState_notProjecting_foregroundTaskChanged_emitsNotShowing() = + testScope.runTest { + mediaRepo.stopProjecting() + val uiState by collectLastValue(viewModel.uiState) + + mediaRepo.switchProjectedTask(createTask(taskId = 1)) + + assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) + } + + @Test + fun uiState_projectingEntireScreen_emitsNotShowing() = + testScope.runTest { + mediaRepo.projectEntireScreen() + val uiState by collectLastValue(viewModel.uiState) + + assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) + } + + @Test + fun uiState_projectingEntireScreen_foregroundTaskChanged_emitsNotShowing() = + testScope.runTest { + mediaRepo.projectEntireScreen() + val uiState by collectLastValue(viewModel.uiState) + + mediaRepo.switchProjectedTask(createTask(taskId = 1)) + + assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) + } + + @Test + fun uiState_projectingTask_foregroundTaskChanged_different_emitsShowing() = + testScope.runTest { + val projectedTask = createTask(taskId = 1) + val foregroundTask = createTask(taskId = 2) + mediaRepo.switchProjectedTask(projectedTask) + val uiState by collectLastValue(viewModel.uiState) + + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + + assertThat(uiState) + .isEqualTo(TaskSwitcherNotificationUiState.Showing(projectedTask, foregroundTask)) + } + + @Test + fun uiState_projectingTask_foregroundTaskChanged_same_emitsNotShowing() = + testScope.runTest { + val projectedTask = createTask(taskId = 1) + mediaRepo.switchProjectedTask(projectedTask) + val uiState by collectLastValue(viewModel.uiState) + + fakeActivityTaskManager.moveTaskToForeground(projectedTask) + + assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) + } + + @Test + fun uiState_projectingTask_foregroundTaskChanged_different_taskIsLauncher_emitsNotShowing() = + testScope.runTest { + val projectedTask = createTask(taskId = 1) + val foregroundTask = createTask(taskId = 2, baseIntent = LAUNCHER_INTENT) + mediaRepo.switchProjectedTask(projectedTask) + val uiState by collectLastValue(viewModel.uiState) + + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + + assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) + } + + companion object { + private val LAUNCHER_INTENT: Intent = + Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) + } +}