diff options
| author | 2023-07-06 11:22:55 +0000 | |
|---|---|---|
| committer | 2023-07-06 11:22:55 +0000 | |
| commit | 52b3af4e777f92c0738c799b7150f918cd563c8d (patch) | |
| tree | e0aee89d00bb45678c4d5895499598776c03093d | |
| parent | c363e64a342a14a27db4d660c6528265ef29fe45 (diff) | |
| parent | 1a61272e999d5e324bf744be911b41f4f7a1d41c (diff) | |
Merge changes Id1c3e56e,Id14bfaed,I3aae76de,I46ff1214,I58a7f648 into udc-qpr-dev am: 3400bb34d5 am: 1a61272e99
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23781543
Change-Id: I0729f5e48085e143249f9831cfece40659dbe9d4
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
23 files changed, 1436 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 40db63d609ba..b1f513d0945c 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -42,6 +42,7 @@ import com.android.systemui.media.dialog.MediaOutputSwitcherDialogUI import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver import com.android.systemui.media.taptotransfer.sender.MediaTttSenderCoordinator +import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherCoreStartable import com.android.systemui.power.PowerUI import com.android.systemui.reardisplay.RearDisplayDialogController import com.android.systemui.recents.Recents @@ -111,6 +112,14 @@ abstract class SystemUICoreStartableModule { @ClassKey(KeyboardUI::class) abstract fun bindKeyboardUI(sysui: KeyboardUI): CoreStartable + /** Inject into MediaProjectionTaskSwitcherCoreStartable. */ + @Binds + @IntoMap + @ClassKey(MediaProjectionTaskSwitcherCoreStartable::class) + abstract fun bindProjectedTaskListener( + sysui: MediaProjectionTaskSwitcherCoreStartable + ): CoreStartable + /** Inject into KeyguardBiometricLockoutLogger */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 06769dc3f831..eef850821166 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -58,6 +58,7 @@ import com.android.systemui.log.dagger.LogModule; import com.android.systemui.log.dagger.MonitorLog; import com.android.systemui.log.table.TableLogBuffer; import com.android.systemui.mediaprojection.appselector.MediaProjectionModule; +import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherModule; import com.android.systemui.model.SysUiState; import com.android.systemui.motiontool.MotionToolModule; import com.android.systemui.navigationbar.NavigationBarComponent; @@ -181,6 +182,7 @@ import javax.inject.Named; LockscreenLayoutModule.class, LogModule.class, MediaProjectionModule.class, + MediaProjectionTaskSwitcherModule.class, MotionToolModule.class, PeopleHubModule.class, PeopleModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartable.kt new file mode 100644 index 000000000000..3c501277ab8c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartable.kt @@ -0,0 +1,39 @@ +/* + * 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 + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.mediaprojection.taskswitcher.ui.TaskSwitcherNotificationCoordinator +import javax.inject.Inject + +@SysUISingleton +class MediaProjectionTaskSwitcherCoreStartable +@Inject +constructor( + private val notificationCoordinator: TaskSwitcherNotificationCoordinator, + private val featureFlags: FeatureFlags, +) : CoreStartable { + + override fun start() { + if (featureFlags.isEnabled(Flags.PARTIAL_SCREEN_SHARING_TASK_SWITCHER)) { + notificationCoordinator.start() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherModule.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherModule.kt new file mode 100644 index 000000000000..22ad07ebc3b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherModule.kt @@ -0,0 +1,32 @@ +/* + * 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 + +import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository +import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository +import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository +import dagger.Binds +import dagger.Module + +@Module +interface MediaProjectionTaskSwitcherModule { + + @Binds fun mediaRepository(impl: MediaProjectionManagerRepository): MediaProjectionRepository + + @Binds fun tasksRepository(impl: ActivityTaskManagerTasksRepository): TasksRepository +} 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 new file mode 100644 index 000000000000..9938f11e5d4c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt @@ -0,0 +1,26 @@ +/* + * 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.model + +import android.app.TaskInfo + +/** Represents the state of media projection. */ +sealed interface MediaProjectionState { + object NotProjecting : MediaProjectionState + object EntireScreen : MediaProjectionState + data class SingleTask(val task: TaskInfo) : 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 new file mode 100644 index 000000000000..492d482459d6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt @@ -0,0 +1,75 @@ +/* + * 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 android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityTaskManager +import android.app.TaskStackListener +import android.os.IBinder +import android.util.Log +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +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 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.withContext + +/** Implementation of [TasksRepository] that uses [ActivityTaskManager] as the data source. */ +@SysUISingleton +class ActivityTaskManagerTasksRepository +@Inject +constructor( + private val activityTaskManager: ActivityTaskManager, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : TasksRepository { + + override suspend fun findRunningTaskFromWindowContainerToken( + windowContainerToken: IBinder + ): RunningTaskInfo? = + getRunningTasks().firstOrNull { taskInfo -> + taskInfo.token.asBinder() == windowContainerToken + } + + private suspend fun getRunningTasks(): List<RunningTaskInfo> = + withContext(backgroundDispatcher) { activityTaskManager.getTasks(Integer.MAX_VALUE) } + + override val foregroundTask: Flow<RunningTaskInfo> = + conflatedCallbackFlow { + val listener = + object : TaskStackListener() { + override fun onTaskMovedToFront(taskInfo: RunningTaskInfo) { + Log.d(TAG, "onTaskMovedToFront: $taskInfo") + trySendWithFailureLogging(taskInfo, TAG) + } + } + activityTaskManager.registerTaskStackListener(listener) + awaitClose { activityTaskManager.unregisterTaskStackListener(listener) } + } + .shareIn(applicationScope, SharingStarted.Lazily, replay = 1) + + companion object { + private const val TAG = "TasksRepository" + } +} 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 new file mode 100644 index 000000000000..38d4e698f2d9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt @@ -0,0 +1,92 @@ +/* + * 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 android.media.projection.MediaProjectionInfo +import android.media.projection.MediaProjectionManager +import android.os.Handler +import android.util.Log +import android.view.ContentRecordingSession +import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +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.Main +import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState +import javax.inject.Inject +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 + +@SysUISingleton +class MediaProjectionManagerRepository +@Inject +constructor( + private val mediaProjectionManager: MediaProjectionManager, + @Main private val handler: Handler, + @Application private val applicationScope: CoroutineScope, + private val tasksRepository: TasksRepository, +) : MediaProjectionRepository { + + override val mediaProjectionState: Flow<MediaProjectionState> = + conflatedCallbackFlow { + val callback = + object : MediaProjectionManager.Callback() { + override fun onStart(info: MediaProjectionInfo?) { + Log.d(TAG, "MediaProjectionManager.Callback#onStart") + trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG) + } + + override fun onStop(info: MediaProjectionInfo?) { + Log.d(TAG, "MediaProjectionManager.Callback#onStop") + trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG) + } + + override fun onRecordingSessionSet( + info: MediaProjectionInfo, + session: ContentRecordingSession? + ) { + Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session") + launch { trySendWithFailureLogging(stateForSession(session), TAG) } + } + } + mediaProjectionManager.addCallback(callback, handler) + awaitClose { mediaProjectionManager.removeCallback(callback) } + } + .shareIn(scope = applicationScope, started = SharingStarted.Lazily, replay = 1) + + private suspend fun stateForSession(session: ContentRecordingSession?): MediaProjectionState { + if (session == null) { + return MediaProjectionState.NotProjecting + } + if (session.contentToRecord == RECORD_CONTENT_DISPLAY || session.tokenToRecord == null) { + return MediaProjectionState.EntireScreen + } + val matchingTask = + tasksRepository.findRunningTaskFromWindowContainerToken(session.tokenToRecord) + ?: return MediaProjectionState.EntireScreen + return MediaProjectionState.SingleTask(matchingTask) + } + + companion object { + private const val TAG = "MediaProjectionMngrRepo" + } +} 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 new file mode 100644 index 000000000000..5bec6925babe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt @@ -0,0 +1,27 @@ +/* + * 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.mediaprojection.taskswitcher.data.model.MediaProjectionState +import kotlinx.coroutines.flow.Flow + +/** Represents a repository to retrieve and change data related to media projection. */ +interface MediaProjectionRepository { + + /** 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 new file mode 100644 index 000000000000..544eb6b99d4f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/NoOpMediaProjectionRepository.kt @@ -0,0 +1,33 @@ +/* + * 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 new file mode 100644 index 000000000000..6a535e4ecc50 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/TasksRepository.kt @@ -0,0 +1,41 @@ +/* + * 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 android.app.ActivityManager.RunningTaskInfo +import android.os.IBinder +import kotlinx.coroutines.flow.Flow + +/** Repository responsible for retrieving data related to running tasks. */ +interface TasksRepository { + + /** + * Tries to find a [RunningTaskInfo] with a matching window container token. Returns `null` when + * no matching task was found. + */ + suspend fun findRunningTaskFromWindowContainerToken( + windowContainerToken: IBinder + ): RunningTaskInfo? + + /** + * Emits a stream of [RunningTaskInfo] that have been moved to the foreground. + * + * Note: when subscribing for the first time, it will not immediately emit the current + * foreground task. Only after a change in foreground task has occurred. + */ + val foregroundTask: Flow<RunningTaskInfo> +} 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 new file mode 100644 index 000000000000..fc5cf7d75bdf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt @@ -0,0 +1,85 @@ +/* + * 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.domain.interactor + +import android.app.TaskInfo +import android.content.Intent +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState +import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository +import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +/** Interactor with logic related to task switching in the context of media projection. */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class TaskSwitchInteractor +@Inject +constructor( + mediaProjectionRepository: MediaProjectionRepository, + private val tasksRepository: TasksRepository, +) { + + /** + * Emits a stream of changes to the state of task switching, in the context of media projection. + */ + val taskSwitchChanges: Flow<TaskSwitchState> = + mediaProjectionRepository.mediaProjectionState.flatMapLatest { projectionState -> + Log.d(TAG, "MediaProjectionState -> $projectionState") + when (projectionState) { + is MediaProjectionState.SingleTask -> { + val projectedTask = projectionState.task + tasksRepository.foregroundTask.map { foregroundTask -> + if (hasForegroundTaskSwitched(projectedTask, foregroundTask)) { + TaskSwitchState.TaskSwitched(projectedTask, foregroundTask) + } else { + TaskSwitchState.TaskUnchanged + } + } + } + is MediaProjectionState.EntireScreen, + is MediaProjectionState.NotProjecting -> { + flowOf(TaskSwitchState.NotProjectingTask) + } + } + } + + /** + * Returns whether tasks have been switched. + * + * Always returns `false` when launcher is in the foreground. The reason is that when going to + * recents to switch apps, launcher becomes the new foreground task, and we don't want to show + * the notification then. + */ + private fun hasForegroundTaskSwitched(projectedTask: TaskInfo, foregroundTask: TaskInfo) = + projectedTask.taskId != foregroundTask.taskId && !foregroundTask.isLauncher + + private val TaskInfo.isLauncher + get() = + baseIntent.hasCategory(Intent.CATEGORY_HOME) && baseIntent.action == Intent.ACTION_MAIN + + companion object { + private const val TAG = "TaskSwitchInteractor" + } +} 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 new file mode 100644 index 000000000000..cd1258ed6aa8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/model/TaskSwitchState.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.domain.model + +import android.app.TaskInfo + +/** Represents tha state of task switching in the context of single task media projection. */ +sealed interface TaskSwitchState { + /** Currently no task is being projected. */ + object NotProjectingTask : TaskSwitchState + /** 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 +} 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 000000000000..a4f407612fa8 --- /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 000000000000..21aee72d17ae --- /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 000000000000..d9754d4429d4 --- /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<TaskSwitcherNotificationUiState> = + 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/MediaProjectionTaskSwitcherCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt new file mode 100644 index 000000000000..bcbf666fd302 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt @@ -0,0 +1,67 @@ +/* + * 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 + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.mediaprojection.taskswitcher.ui.TaskSwitcherNotificationCoordinator +import com.android.systemui.util.mockito.whenever +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class MediaProjectionTaskSwitcherCoreStartableTest : SysuiTestCase() { + + @Mock private lateinit var flags: FeatureFlags + @Mock private lateinit var coordinator: TaskSwitcherNotificationCoordinator + + private lateinit var coreStartable: MediaProjectionTaskSwitcherCoreStartable + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + coreStartable = MediaProjectionTaskSwitcherCoreStartable(coordinator, flags) + } + + @Test + fun start_flagEnabled_startsCoordinator() { + whenever(flags.isEnabled(Flags.PARTIAL_SCREEN_SHARING_TASK_SWITCHER)).thenReturn(true) + + coreStartable.start() + + verify(coordinator).start() + } + + @Test + fun start_flagDisabled_doesNotStartCoordinator() { + whenever(flags.isEnabled(Flags.PARTIAL_SCREEN_SHARING_TASK_SWITCHER)).thenReturn(false) + + coreStartable.start() + + verifyZeroInteractions(coordinator) + } +} 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 new file mode 100644 index 000000000000..83932b0a6133 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt @@ -0,0 +1,108 @@ +/* + * 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 android.os.Binder +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.FakeActivityTaskManager.Companion.createTask +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.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 ActivityTaskManagerTasksRepositoryTest : SysuiTestCase() { + + private val fakeActivityTaskManager = FakeActivityTaskManager() + + private val dispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(dispatcher) + + private val repo = + ActivityTaskManagerTasksRepository( + activityTaskManager = fakeActivityTaskManager.activityTaskManager, + applicationScope = testScope.backgroundScope, + backgroundDispatcher = dispatcher + ) + + @Test + fun findRunningTaskFromWindowContainerToken_noMatch_returnsNull() { + fakeActivityTaskManager.addRunningTasks(createTask(taskId = 1), createTask(taskId = 2)) + + testScope.runTest { + val matchingTask = + repo.findRunningTaskFromWindowContainerToken(windowContainerToken = Binder()) + + assertThat(matchingTask).isNull() + } + } + + @Test + fun findRunningTaskFromWindowContainerToken_matchingToken_returnsTaskInfo() { + val expectedToken = createToken() + val expectedTask = createTask(taskId = 1, token = expectedToken) + + fakeActivityTaskManager.addRunningTasks( + createTask(taskId = 2), + expectedTask, + ) + + testScope.runTest { + val actualTask = + repo.findRunningTaskFromWindowContainerToken( + windowContainerToken = expectedToken.asBinder() + ) + + assertThat(actualTask).isEqualTo(expectedTask) + } + } + + @Test + fun foregroundTask_returnsStreamOfTasksMovedToFront() = + testScope.runTest { + val foregroundTask by collectLastValue(repo.foregroundTask) + + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1)) + assertThat(foregroundTask?.taskId).isEqualTo(1) + + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 2)) + assertThat(foregroundTask?.taskId).isEqualTo(2) + + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 3)) + assertThat(foregroundTask?.taskId).isEqualTo(3) + } + + @Test + fun foregroundTask_lastValueIsCached() = + testScope.runTest { + val foregroundTaskA by collectLastValue(repo.foregroundTask) + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1)) + assertThat(foregroundTaskA?.taskId).isEqualTo(1) + + val foregroundTaskB by collectLastValue(repo.foregroundTask) + assertThat(foregroundTaskB?.taskId).isEqualTo(1) + } +} 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 new file mode 100644 index 000000000000..1c4870bc32b1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt @@ -0,0 +1,77 @@ +/* + * 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 android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityTaskManager +import android.app.TaskStackListener +import android.content.Intent +import android.window.IWindowContainerToken +import android.window.WindowContainerToken +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever + +class FakeActivityTaskManager { + + private val runningTasks = mutableListOf<RunningTaskInfo>() + private val taskTaskListeners = mutableListOf<TaskStackListener>() + + val activityTaskManager = mock<ActivityTaskManager>() + + init { + whenever(activityTaskManager.registerTaskStackListener(any())).thenAnswer { + taskTaskListeners += it.arguments[0] as TaskStackListener + return@thenAnswer Unit + } + whenever(activityTaskManager.unregisterTaskStackListener(any())).thenAnswer { + taskTaskListeners -= it.arguments[0] as TaskStackListener + return@thenAnswer Unit + } + whenever(activityTaskManager.getTasks(any())).thenAnswer { + val maxNumTasks = it.arguments[0] as Int + return@thenAnswer runningTasks.take(maxNumTasks) + } + } + + fun moveTaskToForeground(task: RunningTaskInfo) { + taskTaskListeners.forEach { it.onTaskMovedToFront(task) } + } + + fun addRunningTasks(vararg tasks: RunningTaskInfo) { + runningTasks += tasks + } + + companion object { + + fun createTask( + taskId: Int, + token: WindowContainerToken = createToken(), + baseIntent: Intent = Intent() + ) = + RunningTaskInfo().apply { + this.taskId = taskId + this.token = token + this.baseIntent = baseIntent + } + + fun createToken(): WindowContainerToken { + val realToken = object : IWindowContainerToken.Stub() {} + return WindowContainerToken(realToken) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.kt new file mode 100644 index 000000000000..c59fd60cca9b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.kt @@ -0,0 +1,42 @@ +/* + * 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 android.app.TaskInfo +import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeMediaProjectionRepository : MediaProjectionRepository { + + private val state = MutableStateFlow<MediaProjectionState>(MediaProjectionState.NotProjecting) + + fun switchProjectedTask(newTask: TaskInfo) { + state.value = MediaProjectionState.SingleTask(newTask) + } + + override val mediaProjectionState: Flow<MediaProjectionState> = state.asStateFlow() + + fun projectEntireScreen() { + state.value = MediaProjectionState.EntireScreen + } + + fun stopProjecting() { + state.value = MediaProjectionState.NotProjecting + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt new file mode 100644 index 000000000000..593e3893fb2a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt @@ -0,0 +1,68 @@ +/* + * 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 android.app.ActivityManager.RunningTaskInfo +import android.content.Intent +import android.os.IBinder +import android.window.IWindowContainerToken +import android.window.WindowContainerToken +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeTasksRepository : TasksRepository { + + private val _foregroundTask = MutableStateFlow(DEFAULT_TASK) + + override val foregroundTask: Flow<RunningTaskInfo> = _foregroundTask.asStateFlow() + + private val runningTasks = mutableListOf(DEFAULT_TASK) + + override suspend fun findRunningTaskFromWindowContainerToken( + windowContainerToken: IBinder + ): RunningTaskInfo? = runningTasks.firstOrNull { it.token.asBinder() == windowContainerToken } + + fun addRunningTask(task: RunningTaskInfo) { + runningTasks.add(task) + } + + fun moveTaskToForeground(task: RunningTaskInfo) { + _foregroundTask.value = task + } + + companion object { + val DEFAULT_TASK = createTask(taskId = -1) + val LAUNCHER_INTENT: Intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) + + fun createTask( + taskId: Int, + token: WindowContainerToken = createToken(), + baseIntent: Intent = Intent() + ) = + RunningTaskInfo().apply { + this.taskId = taskId + this.token = token + this.baseIntent = baseIntent + } + + fun createToken(): WindowContainerToken { + val realToken = object : IWindowContainerToken.Stub() {} + return WindowContainerToken(realToken) + } + } +} 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 new file mode 100644 index 000000000000..2b074655bb02 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt @@ -0,0 +1,162 @@ +/* + * 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 android.media.projection.MediaProjectionInfo +import android.media.projection.MediaProjectionManager +import android.os.Binder +import android.os.Handler +import android.os.UserHandle +import android.testing.AndroidTestingRunner +import android.view.ContentRecordingSession +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +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.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +@SmallTest +class MediaProjectionManagerRepositoryTest : SysuiTestCase() { + + private val mediaProjectionManager = mock<MediaProjectionManager>() + + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + private val tasksRepo = FakeTasksRepository() + + private lateinit var callback: MediaProjectionManager.Callback + private lateinit var repo: MediaProjectionManagerRepository + + @Before + fun setUp() { + whenever(mediaProjectionManager.addCallback(any(), any())).thenAnswer { + callback = it.arguments[0] as MediaProjectionManager.Callback + return@thenAnswer Unit + } + repo = + MediaProjectionManagerRepository( + mediaProjectionManager = mediaProjectionManager, + handler = Handler.getMain(), + applicationScope = testScope.backgroundScope, + tasksRepository = tasksRepo + ) + } + + @Test + fun mediaProjectionState_onStart_emitsNotProjecting() = + testScope.runTest { + val state by collectLastValue(repo.mediaProjectionState) + runCurrent() + + callback.onStart(TEST_MEDIA_INFO) + + assertThat(state).isEqualTo(MediaProjectionState.NotProjecting) + } + + @Test + fun mediaProjectionState_onStop_emitsNotProjecting() = + testScope.runTest { + val state by collectLastValue(repo.mediaProjectionState) + runCurrent() + + callback.onStop(TEST_MEDIA_INFO) + + assertThat(state).isEqualTo(MediaProjectionState.NotProjecting) + } + + @Test + fun mediaProjectionState_onSessionSet_sessionNull_emitsNotProjecting() = + testScope.runTest { + val state by collectLastValue(repo.mediaProjectionState) + runCurrent() + + callback.onRecordingSessionSet(TEST_MEDIA_INFO, /* session= */ null) + + assertThat(state).isEqualTo(MediaProjectionState.NotProjecting) + } + + @Test + fun mediaProjectionState_onSessionSet_contentToRecordDisplay_emitsEntireScreen() = + testScope.runTest { + val state by collectLastValue(repo.mediaProjectionState) + runCurrent() + + val session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123) + callback.onRecordingSessionSet(TEST_MEDIA_INFO, session) + + assertThat(state).isEqualTo(MediaProjectionState.EntireScreen) + } + + @Test + fun mediaProjectionState_onSessionSet_tokenNull_emitsEntireScreen() = + testScope.runTest { + val state by collectLastValue(repo.mediaProjectionState) + runCurrent() + + val session = + ContentRecordingSession.createTaskSession(/* taskWindowContainerToken= */ null) + callback.onRecordingSessionSet(TEST_MEDIA_INFO, session) + + assertThat(state).isEqualTo(MediaProjectionState.EntireScreen) + } + + @Test + fun mediaProjectionState_sessionSet_taskWithToken_noMatchingRunningTask_emitsEntireScreen() = + testScope.runTest { + val state by collectLastValue(repo.mediaProjectionState) + runCurrent() + + val taskWindowContainerToken = Binder() + val session = ContentRecordingSession.createTaskSession(taskWindowContainerToken) + callback.onRecordingSessionSet(TEST_MEDIA_INFO, session) + + assertThat(state).isEqualTo(MediaProjectionState.EntireScreen) + } + + @Test + fun mediaProjectionState_sessionSet_taskWithToken_matchingRunningTask_emitsSingleTask() = + testScope.runTest { + val token = FakeTasksRepository.createToken() + val task = FakeTasksRepository.createTask(taskId = 1, token = token) + tasksRepo.addRunningTask(task) + val state by collectLastValue(repo.mediaProjectionState) + runCurrent() + + val session = ContentRecordingSession.createTaskSession(token.asBinder()) + callback.onRecordingSessionSet(TEST_MEDIA_INFO, session) + + assertThat(state).isEqualTo(MediaProjectionState.SingleTask(task)) + } + + companion object { + val TEST_MEDIA_INFO = + MediaProjectionInfo(/* packageName= */ "com.test.package", UserHandle.CURRENT) + } +} 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 new file mode 100644 index 000000000000..112950b860e8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt @@ -0,0 +1,127 @@ +/* + * 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.domain.interactor + +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.model.TaskSwitchState +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 TaskSwitchInteractorTest : 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) + + @Test + fun taskSwitchChanges_notProjecting_foregroundTaskChange_emitsNotProjectingTask() = + testScope.runTest { + mediaRepo.stopProjecting() + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1)) + + assertThat(taskSwitchState).isEqualTo(TaskSwitchState.NotProjectingTask) + } + + @Test + fun taskSwitchChanges_projectingScreen_foregroundTaskChange_emitsNotProjectingTask() = + testScope.runTest { + mediaRepo.projectEntireScreen() + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1)) + + assertThat(taskSwitchState).isEqualTo(TaskSwitchState.NotProjectingTask) + } + + @Test + fun taskSwitchChanges_projectingTask_foregroundTaskDifferent_emitsTaskChanged() = + testScope.runTest { + val projectedTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 1) + mediaRepo.switchProjectedTask(projectedTask) + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + + assertThat(taskSwitchState) + .isEqualTo( + TaskSwitchState.TaskSwitched( + projectedTask = projectedTask, + foregroundTask = foregroundTask + ) + ) + } + + @Test + fun taskSwitchChanges_projectingTask_foregroundTaskLauncher_emitsTaskUnchanged() = + testScope.runTest { + val projectedTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 1, baseIntent = LAUNCHER_INTENT) + mediaRepo.switchProjectedTask(projectedTask) + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + + assertThat(taskSwitchState).isEqualTo(TaskSwitchState.TaskUnchanged) + } + + @Test + fun taskSwitchChanges_projectingTask_foregroundTaskSame_emitsTaskUnchanged() = + testScope.runTest { + val projectedTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 0) + mediaRepo.switchProjectedTask(projectedTask) + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + + assertThat(taskSwitchState).isEqualTo(TaskSwitchState.TaskUnchanged) + } + + companion object { + private val LAUNCHER_INTENT: Intent = + Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) + } +} 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 000000000000..ea44fb3b1f6e --- /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) + } +} |