Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0f4a2604 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[SB][Screen Chips] Distinguish share-to-app and cast-to-other-device.

The share-to-app chip has a different icon from the cast-to-other-device
chip. This CL adds a way to distinguish them, and shows chips with
different icons accordingly.

Bug: 332662551
Flag: com.android.systemui.status_bar_screen_sharing_chips
Test: Use the Cast QS tile to cast to a different device -> verify chip
appears with the cast icon
Test: Share your screen with an app -> verify chip appears with the
share icon (real icon coming later)
Test: atest MediaProjectionManagerRepositoryTest
MediaProjectionChipInteractorTest

Change-Id: Ic22cc8a6ba0a10ec9e03fc3e9a257b8b729fcbb3
parent 3419d693
Loading
Loading
Loading
Loading
+17 −3
Original line number Diff line number Diff line
@@ -20,7 +20,21 @@ import android.app.ActivityManager.RunningTaskInfo

/** Represents the state of media projection. */
sealed interface MediaProjectionState {
    object NotProjecting : MediaProjectionState
    object EntireScreen : MediaProjectionState
    data class SingleTask(val task: RunningTaskInfo) : MediaProjectionState
    /** There is no media being projected. */
    data object NotProjecting : MediaProjectionState

    /**
     * Media is currently being projected.
     *
     * @property hostPackage the package name of the app that is receiving the content of the media
     *   projection (aka which app the phone screen contents are being sent to).
     */
    sealed class Projecting(open val hostPackage: String) : MediaProjectionState {
        /** The entire screen is being projected. */
        data class EntireScreen(override val hostPackage: String) : Projecting(hostPackage)

        /** Only a single task is being projected. */
        data class SingleTask(override val hostPackage: String, val task: RunningTaskInfo) :
            Projecting(hostPackage)
    }
}
+12 −6
Original line number Diff line number Diff line
@@ -83,7 +83,9 @@ constructor(
                            session: ContentRecordingSession?
                        ) {
                            Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session")
                            launch { trySendWithFailureLogging(stateForSession(session), TAG) }
                            launch {
                                trySendWithFailureLogging(stateForSession(info, session), TAG)
                            }
                        }
                    }
                mediaProjectionManager.addCallback(callback, handler)
@@ -95,19 +97,23 @@ constructor(
                initialValue = MediaProjectionState.NotProjecting,
            )

    private suspend fun stateForSession(session: ContentRecordingSession?): MediaProjectionState {
    private suspend fun stateForSession(
        info: MediaProjectionInfo,
        session: ContentRecordingSession?
    ): MediaProjectionState {
        if (session == null) {
            return MediaProjectionState.NotProjecting
        }

        val hostPackage = info.packageName
        if (session.contentToRecord == RECORD_CONTENT_DISPLAY || session.tokenToRecord == null) {
            return MediaProjectionState.EntireScreen
            return MediaProjectionState.Projecting.EntireScreen(hostPackage)
        }
        val matchingTask =
            tasksRepository.findRunningTaskFromWindowContainerToken(
                checkNotNull(session.tokenToRecord)
            )
                ?: return MediaProjectionState.EntireScreen
        return MediaProjectionState.SingleTask(matchingTask)
            ) ?: return MediaProjectionState.Projecting.EntireScreen(hostPackage)
        return MediaProjectionState.Projecting.SingleTask(hostPackage, matchingTask)
    }

    companion object {
+2 −2
Original line number Diff line number Diff line
@@ -57,7 +57,7 @@ constructor(
        mediaProjectionRepository.mediaProjectionState.flatMapLatest { projectionState ->
            Log.d(TAG, "MediaProjectionState -> $projectionState")
            when (projectionState) {
                is MediaProjectionState.SingleTask -> {
                is MediaProjectionState.Projecting.SingleTask -> {
                    val projectedTask = projectionState.task
                    tasksRepository.foregroundTask.map { foregroundTask ->
                        if (hasForegroundTaskSwitched(projectedTask, foregroundTask)) {
@@ -67,7 +67,7 @@ constructor(
                        }
                    }
                }
                is MediaProjectionState.EntireScreen,
                is MediaProjectionState.Projecting.EntireScreen,
                is MediaProjectionState.NotProjecting -> {
                    flowOf(TaskSwitchState.NotProjectingTask)
                }
+51 −16
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor

import android.content.pm.PackageManager
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
@@ -25,6 +26,7 @@ import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepos
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
import com.android.systemui.util.Utils
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -49,31 +51,64 @@ class MediaProjectionChipInteractor
constructor(
    @Application scope: CoroutineScope,
    mediaProjectionRepository: MediaProjectionRepository,
    val systemClock: SystemClock,
    private val packageManager: PackageManager,
    private val systemClock: SystemClock,
) : OngoingActivityChipInteractor {
    override val chip: StateFlow<OngoingActivityChipModel> =
        mediaProjectionRepository.mediaProjectionState
            .map { state ->
                when (state) {
                    is MediaProjectionState.NotProjecting -> OngoingActivityChipModel.Hidden
                    is MediaProjectionState.EntireScreen,
                    is MediaProjectionState.SingleTask -> {
                        // TODO(b/332662551): Distinguish between cast-to-other-device and
                        // share-to-app.
                        OngoingActivityChipModel.Shown(
                    is MediaProjectionState.Projecting -> {
                        if (isProjectionToOtherDevice(state.hostPackage)) {
                            createCastToOtherDeviceChip()
                        } else {
                            createShareToAppChip()
                        }
                    }
                }
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)

    /**
     * Returns true iff projecting to the given [packageName] means that we're projecting to a
     * *different* device (as opposed to projecting to some application on *this* device).
     */
    private fun isProjectionToOtherDevice(packageName: String?): Boolean {
        // The [isHeadlessRemoteDisplayProvider] check approximates whether a projection is to a
        // different device or the same device, because headless remote display packages are the
        // only kinds of packages that do cast-to-other-device. This isn't exactly perfect,
        // because it means that any projection by those headless remote display packages will be
        // marked as going to a different device, even if that isn't always true. See b/321078669.
        return Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName)
    }

    private fun createCastToOtherDeviceChip(): OngoingActivityChipModel.Shown {
        return OngoingActivityChipModel.Shown(
            icon =
                Icon.Resource(
                    R.drawable.ic_cast_connected,
                    ContentDescription.Resource(R.string.accessibility_casting)
                ),
                            // TODO(b/332662551): See if we can use a MediaProjection API to fetch
                            // this time.
            // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
            startTimeMs = systemClock.elapsedRealtime()
        ) {
            // TODO(b/332662551): Implement the pause dialog.
        }
    }

    private fun createShareToAppChip(): OngoingActivityChipModel.Shown {
        return OngoingActivityChipModel.Shown(
            icon =
                Icon.Resource(
                    // TODO(b/332662551): Use the right icon and content description.
                    R.drawable.ic_screenshot_share,
                    contentDescription = null,
                ),
            // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
            startTimeMs = systemClock.elapsedRealtime()
        ) {
            // TODO(b/332662551): Implement the pause dialog.
        }
    }
            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
}
+53 −4
Original line number Diff line number Diff line
@@ -16,7 +16,9 @@

package com.android.systemui.mediaprojection.data.repository

import android.media.projection.MediaProjectionInfo
import android.os.Binder
import android.os.UserHandle
import android.view.ContentRecordingSession
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -26,6 +28,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createDisplaySession
import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
@@ -55,7 +58,8 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() {
            fakeActivityTaskManager.addRunningTasks(task)
            repo.switchProjectedTask(task)

            assertThat(state).isEqualTo(MediaProjectionState.SingleTask(task))
            assertThat(state).isInstanceOf(MediaProjectionState.Projecting.SingleTask::class.java)
            assertThat((state as MediaProjectionState.Projecting.SingleTask).task).isEqualTo(task)
        }

    @Test
@@ -97,7 +101,7 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() {
                session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123)
            )

            assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
            assertThat(state).isInstanceOf(MediaProjectionState.Projecting.EntireScreen::class.java)
        }

    @Test
@@ -110,7 +114,27 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() {
                session = ContentRecordingSession.createTaskSession(taskWindowContainerToken)
            )

            assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
            assertThat(state).isInstanceOf(MediaProjectionState.Projecting.EntireScreen::class.java)
        }

    @Test
    fun mediaProjectionState_entireScreen_hasHostPackage() =
        testScope.runTest {
            val state by collectLastValue(repo.mediaProjectionState)

            val info =
                MediaProjectionInfo(
                    /* packageName= */ "com.media.projection.repository.test",
                    /* handle= */ UserHandle.getUserHandleForUid(UserHandle.myUserId()),
                    /* launchCookie = */ null,
                )
            fakeMediaProjectionManager.dispatchOnSessionSet(
                info = info,
                session = createDisplaySession(),
            )

            assertThat((state as MediaProjectionState.Projecting.EntireScreen).hostPackage)
                .isEqualTo("com.media.projection.repository.test")
        }

    @Test
@@ -125,6 +149,31 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() {
                session = ContentRecordingSession.createTaskSession(token.asBinder())
            )

            assertThat(state).isEqualTo(MediaProjectionState.SingleTask(task))
            assertThat(state).isInstanceOf(MediaProjectionState.Projecting.SingleTask::class.java)
            assertThat((state as MediaProjectionState.Projecting.SingleTask).task).isEqualTo(task)
        }

    @Test
    fun mediaProjectionState_singleTask_hasHostPackage() =
        testScope.runTest {
            val state by collectLastValue(repo.mediaProjectionState)

            val token = createToken()
            val task = createTask(taskId = 1, token = token)
            fakeActivityTaskManager.addRunningTasks(task)

            val info =
                MediaProjectionInfo(
                    /* packageName= */ "com.media.projection.repository.test",
                    /* handle= */ UserHandle.getUserHandleForUid(UserHandle.myUserId()),
                    /* launchCookie = */ null,
                )
            fakeMediaProjectionManager.dispatchOnSessionSet(
                info = info,
                session = ContentRecordingSession.createTaskSession(token.asBinder())
            )

            assertThat((state as MediaProjectionState.Projecting.SingleTask).hostPackage)
                .isEqualTo("com.media.projection.repository.test")
        }
}
Loading