Merge changes Id1c3e56e,Id14bfaed,I3aae76de,I46ff1214,I58a7f648 into udc-qpr-dev

* changes:
  Partial Screen Sharing: Task Switcher - Media Projection State Repo
  Partial Screen Sharing: Task Switcher - Dagger Module and Core Startable
  Partial Screen Sharing: Task Switcher - UI Layer implementation
  Partial Screen Sharing: Task Switcher - Domain Layer implementation
  Partial Screen Sharing: Task Switcher Data Layer partial implementation
This commit is contained in:
Chris Göllner
2023-07-06 10:51:24 +00:00
committed by Android (Google) Code Review
23 changed files with 1436 additions and 0 deletions

View File

@@ -42,6 +42,7 @@ import com.android.systemui.media.dialog.MediaOutputSwitcherDialogUI
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper
import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
import com.android.systemui.media.taptotransfer.sender.MediaTttSenderCoordinator 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.power.PowerUI
import com.android.systemui.reardisplay.RearDisplayDialogController import com.android.systemui.reardisplay.RearDisplayDialogController
import com.android.systemui.recents.Recents import com.android.systemui.recents.Recents
@@ -111,6 +112,14 @@ abstract class SystemUICoreStartableModule {
@ClassKey(KeyboardUI::class) @ClassKey(KeyboardUI::class)
abstract fun bindKeyboardUI(sysui: KeyboardUI): CoreStartable abstract fun bindKeyboardUI(sysui: KeyboardUI): CoreStartable
/** Inject into MediaProjectionTaskSwitcherCoreStartable. */
@Binds
@IntoMap
@ClassKey(MediaProjectionTaskSwitcherCoreStartable::class)
abstract fun bindProjectedTaskListener(
sysui: MediaProjectionTaskSwitcherCoreStartable
): CoreStartable
/** Inject into KeyguardBiometricLockoutLogger */ /** Inject into KeyguardBiometricLockoutLogger */
@Binds @Binds
@IntoMap @IntoMap

View File

@@ -59,6 +59,7 @@ import com.android.systemui.log.dagger.LogModule;
import com.android.systemui.log.dagger.MonitorLog; import com.android.systemui.log.dagger.MonitorLog;
import com.android.systemui.log.table.TableLogBuffer; import com.android.systemui.log.table.TableLogBuffer;
import com.android.systemui.mediaprojection.appselector.MediaProjectionModule; import com.android.systemui.mediaprojection.appselector.MediaProjectionModule;
import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherModule;
import com.android.systemui.model.SysUiState; import com.android.systemui.model.SysUiState;
import com.android.systemui.motiontool.MotionToolModule; import com.android.systemui.motiontool.MotionToolModule;
import com.android.systemui.navigationbar.NavigationBarComponent; import com.android.systemui.navigationbar.NavigationBarComponent;
@@ -182,6 +183,7 @@ import javax.inject.Named;
LockscreenLayoutModule.class, LockscreenLayoutModule.class,
LogModule.class, LogModule.class,
MediaProjectionModule.class, MediaProjectionModule.class,
MediaProjectionTaskSwitcherModule.class,
MotionToolModule.class, MotionToolModule.class,
PeopleHubModule.class, PeopleHubModule.class,
PeopleModule.class, PeopleModule.class,

View File

@@ -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()
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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>
}

View File

@@ -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()
}

View File

@@ -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>
}

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}