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

Commit 44f67875 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[SB][Screen Chips] Show cast chip even if audio-only casting.

When casting to another device, you can either cast your screen, or just
your audio.
 - When casting just audio, only MediaRouter APIs fire.
 - When casting screen, both MediaRouter APIs and MediaProjection APIs
   fire.
Previously, we were only listening to MediaProjection APIs, so we
weren't showing the cast chip if you're just casting audio. This CL adds
listening to MediaRouter events so that we also show the cast chip if
you're casting just audio.

This gets tricky when casting your screen though, because both the
MediaProjection APIs and the MediaRouter APIs fire when screen casting
but they fire at different times (see b/269975671):
- The MediaRouter APIs fire as soon as you select a device to cast to,
  even if you haven't confirmed you want to start casting.
- The MediaProjection APIs only fire once you've confirmed casting
  should start.
This CL also includes disambiguation logic between these two API surfaces.

Key changes:
1) Adds new MediaRouterRepository, which listens to the existing
   CastController for MediaRouter events.
2) Adds new MediaRouterChipInteractor, which filters those media router
   events down to just active casts.
3) Updates CastToOtherDeviceChipViewModel to listen to the new
   interactor and use either MediaRouter or MediaProjection as the
   datasource, depending on what's available.
2) Updates the stop dialog to have screen-specific strings or generic
   strings, depending on whether MediaProjection is active.

Bug: 332662551
Flag: com.android.systemui.status_bar_screen_sharing_chips

Test: Cast to an audio-only device -> verify cast chip appears. Tap chip
-> verify dialog text is audio-specific. Tap "Stop casting" in dialog ->
verify casting stops.

Test: Cast to an audio-only device, then also start sharing your screen
to an app -> verify the cast-to-other-device chip is replaced by the
share-to-app chip. Tap chip -> verify dialog is about sharing, not
casting. Stop sharing to app -> verify cast-to-other-device chip
re-appears. Tap chip -> verify dialog is about casting.

Test: Cast screen to a device -> verify cast chip appears. Verify timer
doesn't reset between selecting the device and starting casting. Tap
chip -> verify dialog text is screen-specific. Tap "Stop casting" in
dialog -> verify casting stops.

Test: atest MediaRouterRepositoryTest MediaRouterChipInteractorTest
CastToOtherDeviceChipViewModelTest

Change-Id: I4a1c6ea9e1089d3780b6e0a5d6cb88675e6a7aa0
parent 72cd9a53
Loading
Loading
Loading
Loading
+8 −4
Original line number Diff line number Diff line
@@ -341,13 +341,17 @@
    <string name="share_to_app_stop_dialog_button">Stop sharing</string>

    <!-- Content description for the status bar chip shown to the user when they're casting their screen to a different device [CHAR LIMIT=NONE] -->
    <string name="cast_to_other_device_chip_accessibility_label">Casting screen</string>
    <string name="cast_screen_to_other_device_chip_accessibility_label">Casting screen</string>
    <!-- Title for a dialog shown to the user that will let them stop casting their screen to a different device [CHAR LIMIT=50] -->
    <string name="cast_to_other_device_stop_dialog_title">Stop casting screen?</string>
    <string name="cast_screen_to_other_device_stop_dialog_title">Stop casting screen?</string>
    <!-- Title for a dialog shown to the user that will let them stop casting to a different device [CHAR LIMIT=50] -->
    <string name="cast_to_other_device_stop_dialog_title">Stop casting?</string>
    <!-- Text telling a user that they will stop casting their screen to a different device if they click the "Stop casting" button [CHAR LIMIT=100] -->
    <string name="cast_to_other_device_stop_dialog_message">You will stop casting your screen</string>
    <string name="cast_screen_to_other_device_stop_dialog_message">You will stop casting your screen</string>
    <!-- Text telling a user that they will stop casting the contents of the specified [app_name] to a different device if they click the "Stop casting" button. Note that the app name will appear in bold.  [CHAR LIMIT=100] -->
    <string name="cast_to_other_device_stop_dialog_message_specific_app">You will stop casting &lt;b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g>&lt;/b></string>
    <string name="cast_screen_to_other_device_stop_dialog_message_specific_app">You will stop casting &lt;b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g>&lt;/b></string>
    <!-- Text telling a user that they're currently casting to a different device [CHAR LIMIT=100] -->
    <string name="cast_to_other_device_stop_dialog_message">You\'re currently casting</string>
    <!-- Button to stop screen casting to a different device [CHAR LIMIT=35] -->
    <string name="cast_to_other_device_stop_dialog_button">Stop casting</string>

+2 −0
Original line number Diff line number Diff line
@@ -74,6 +74,7 @@ import com.android.systemui.log.table.TableLogBuffer;
import com.android.systemui.mediaprojection.MediaProjectionModule;
import com.android.systemui.mediaprojection.appselector.MediaProjectionActivitiesModule;
import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherModule;
import com.android.systemui.mediarouter.MediaRouterModule;
import com.android.systemui.model.SceneContainerPlugin;
import com.android.systemui.model.SysUiState;
import com.android.systemui.motiontool.MotionToolModule;
@@ -220,6 +221,7 @@ import javax.inject.Named;
        MediaProjectionActivitiesModule.class,
        MediaProjectionModule.class,
        MediaProjectionTaskSwitcherModule.class,
        MediaRouterModule.class,
        MotionToolModule.class,
        NotificationIconAreaControllerModule.class,
        PeopleHubModule.class,
+27 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.mediarouter

import com.android.systemui.mediarouter.data.repository.MediaRouterRepository
import com.android.systemui.mediarouter.data.repository.MediaRouterRepositoryImpl
import dagger.Binds
import dagger.Module

@Module
interface MediaRouterModule {
    @Binds fun mediaRouterRepository(impl: MediaRouterRepositoryImpl): MediaRouterRepository
}
+65 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.mediarouter.data.repository

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.policy.CastController
import com.android.systemui.statusbar.policy.CastDevice
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn

/** A repository for data coming from MediaRouter APIs. */
interface MediaRouterRepository {
    /** A list of the cast devices that MediaRouter is currently aware of. */
    val castDevices: StateFlow<List<CastDevice>>

    /** Stops the cast to the given device. */
    fun stopCasting(device: CastDevice)
}

@SysUISingleton
class MediaRouterRepositoryImpl
@Inject
constructor(
    @Application private val scope: CoroutineScope,
    private val castController: CastController,
) : MediaRouterRepository {
    override val castDevices: StateFlow<List<CastDevice>> =
        conflatedCallbackFlow {
                val callback =
                    CastController.Callback {
                        val mediaRouterCastDevices =
                            castController.castDevices.filter {
                                it.origin == CastDevice.CastOrigin.MediaRouter
                            }
                        trySend(mediaRouterCastDevices)
                    }
                castController.addCallback(callback)
                awaitClose { castController.removeCallback(callback) }
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())

    override fun stopCasting(device: CastDevice) {
        castController.stopCasting(device)
    }
}
+63 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.statusbar.chips.casttootherdevice.domain.interactor

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.mediarouter.data.repository.MediaRouterRepository
import com.android.systemui.statusbar.chips.casttootherdevice.domain.model.MediaRouterCastModel
import com.android.systemui.statusbar.policy.CastDevice
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

/**
 * Interactor for MediaRouter events, used to show the cast-audio-to-other-device chip in the status
 * bar.
 */
@SysUISingleton
class MediaRouterChipInteractor
@Inject
constructor(
    @Application private val scope: CoroutineScope,
    private val mediaRouterRepository: MediaRouterRepository,
) {
    private val activeCastDevice: StateFlow<CastDevice?> =
        mediaRouterRepository.castDevices
            .map { allDevices -> allDevices.firstOrNull { it.isCasting } }
            .stateIn(scope, SharingStarted.WhileSubscribed(), null)

    /** The current casting state, according to MediaRouter APIs. */
    val mediaRouterCastingState: StateFlow<MediaRouterCastModel> =
        activeCastDevice
            .map {
                if (it != null) {
                    MediaRouterCastModel.Casting
                } else {
                    MediaRouterCastModel.DoingNothing
                }
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), MediaRouterCastModel.DoingNothing)

    /** Stops the currently active MediaRouter cast. */
    fun stopCasting() {
        activeCastDevice.value?.let { mediaRouterRepository.stopCasting(it) }
    }
}
Loading