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

Commit bf875df5 authored by Beth Thibodeau's avatar Beth Thibodeau Committed by Android (Google) Code Review
Browse files

Merge "Fix device name issues for system sessions" into main

parents 0aa032ee 5e59a934
Loading
Loading
Loading
Loading
+30 −23
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;

import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;

import android.annotation.NonNull;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.media.MediaRoute2Info;
@@ -51,6 +52,34 @@ public class PhoneMediaDevice extends MediaDevice {

    private final DeviceIconUtil mDeviceIconUtil;

    /** Returns the device name for the given {@code routeInfo}. */
    public static String getSystemRouteNameFromType(
            @NonNull Context context, @NonNull MediaRoute2Info routeInfo) {
        CharSequence name;
        switch (routeInfo.getType()) {
            case TYPE_WIRED_HEADSET:
            case TYPE_WIRED_HEADPHONES:
            case TYPE_USB_DEVICE:
            case TYPE_USB_HEADSET:
            case TYPE_USB_ACCESSORY:
                name = context.getString(R.string.media_transfer_wired_usb_device_name);
                break;
            case TYPE_DOCK:
                name = context.getString(R.string.media_transfer_dock_speaker_device_name);
                break;
            case TYPE_BUILTIN_SPEAKER:
                name = context.getString(R.string.media_transfer_this_device_name);
                break;
            case TYPE_HDMI:
                name = context.getString(R.string.media_transfer_external_device_name);
                break;
            default:
                name = context.getString(R.string.media_transfer_default_device_name);
                break;
        }
        return name.toString();
    }

    PhoneMediaDevice(Context context, MediaRoute2Info info, String packageName) {
        this(context, info, packageName, null);
    }
@@ -69,29 +98,7 @@ public class PhoneMediaDevice extends MediaDevice {
    @SuppressWarnings("NewApi")
    @Override
    public String getName() {
        CharSequence name;
        switch (mRouteInfo.getType()) {
            case TYPE_WIRED_HEADSET:
            case TYPE_WIRED_HEADPHONES:
            case TYPE_USB_DEVICE:
            case TYPE_USB_HEADSET:
            case TYPE_USB_ACCESSORY:
                name = mContext.getString(R.string.media_transfer_wired_usb_device_name);
                break;
            case TYPE_DOCK:
                name = mContext.getString(R.string.media_transfer_dock_speaker_device_name);
                break;
            case TYPE_BUILTIN_SPEAKER:
                name = mContext.getString(R.string.media_transfer_this_device_name);
                break;
            case TYPE_HDMI:
                name = mContext.getString(R.string.media_transfer_external_device_name);
                break;
            default:
                name = mContext.getString(R.string.media_transfer_default_device_name);
                break;
        }
        return name.toString();
        return getSystemRouteNameFromType(mContext, mRouteInfo);
    }

    @Override
+3 −0
Original line number Diff line number Diff line
@@ -473,6 +473,9 @@ object Flags {
    // TODO(b/270437894): Tracking Bug
    val MEDIA_REMOTE_RESUME = unreleasedFlag("media_remote_resume")

    // TODO(b/304506662): Tracking Bug
    val MEDIA_DEVICE_NAME_FIX = unreleasedFlag("media_device_name_fix", teamfood = true)

    // 1000 - dock
    val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag("simulate_dock_through_charging")

+67 −10
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.bluetooth.BluetoothLeBroadcastMetadata
import android.content.Context
import android.graphics.drawable.Drawable
import android.media.MediaRouter2Manager
import android.media.RoutingSessionInfo
import android.media.session.MediaController
import android.text.TextUtils
import android.util.Log
@@ -31,17 +32,20 @@ import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.media.PhoneMediaDevice
import com.android.systemui.Dumpable
import com.android.systemui.res.R
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.player.MediaDeviceData
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.ConfigurationController
import java.io.PrintWriter
import java.util.concurrent.Executor
@@ -64,7 +68,8 @@ constructor(
    private val localBluetoothManager: LocalBluetoothManager?,
    @Main private val fgExecutor: Executor,
    @Background private val bgExecutor: Executor,
    dumpManager: DumpManager
    dumpManager: DumpManager,
    private val featureFlags: FeatureFlagsClassic,
) : MediaDataManager.Listener, Dumpable {

    private val listeners: MutableSet<Listener> = mutableSetOf()
@@ -215,6 +220,7 @@ constructor(
                println("    volumeControlId=$volumeControlId cached= $playbackVolumeControlId")
                println("    routingSession=$routingSession")
                println("    selectedRoutes=$selectedRoutes")
                println("    currentConnectedDevice=${localMediaManager.currentConnectedDevice}")
            }
        }

@@ -348,15 +354,15 @@ constructor(
                }
                val device =
                    aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice
                val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
                val routingSession =
                    controller?.let { mr2manager.getRoutingSessionForMediaController(it) }

                // If we have a controller but get a null route, then don't trust the device
                val enabled = device != null && (controller == null || route != null)
                val name =
                    if (controller == null || route != null) {
                        route?.name?.toString() ?: device?.name
                    } else {
                        null
                val enabled = device != null && (controller == null || routingSession != null)

                val name = getDeviceName(device, routingSession)
                if (DEBUG) {
                    Log.d(TAG, "new device name $name")
                }
                current =
                    MediaDeviceData(
@@ -369,6 +375,57 @@ constructor(
            }
        }

        /** Return a display name for the current device / route, or null if not possible */
        private fun getDeviceName(
            device: MediaDevice?,
            routingSession: RoutingSessionInfo?,
        ): String? {
            val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) }

            if (DEBUG) {
                Log.d(
                    TAG,
                    "device is $device, controller $controller," +
                        " routingSession ${routingSession?.name}" +
                        " or ${selectedRoutes?.firstOrNull()?.name}"
                )
            }

            if (!featureFlags.isEnabled(Flags.MEDIA_DEVICE_NAME_FIX)) {
                if (controller == null || routingSession != null) {
                    return routingSession?.name?.toString() ?: device?.name
                }
                return null
            }

            if (controller == null) {
                // In resume state, we don't have a controller - just use the device name
                return device?.name
            }

            if (routingSession == null) {
                // This happens when casting from apps that do not support MediaRouter2
                // The output switcher can't show anything useful here, so set to null
                return null
            }

            // If this is a user route (app / cast provided), use the provided name
            if (!routingSession.isSystemSession) {
                return routingSession.name?.toString() ?: device?.name
            }

            selectedRoutes?.firstOrNull()?.let {
                if (device is PhoneMediaDevice) {
                    // Get the (localized) name for this phone device
                    return PhoneMediaDevice.getSystemRouteNameFromType(context, it)
                } else {
                    // If it's another type of device (in practice, Bluetooth), use the route name
                    return it.name.toString()
                }
            }
            return null
        }

        private fun isLeAudioBroadcastEnabled(): Boolean {
            if (localBluetoothManager != null) {
                val profileManager = localBluetoothManager.profileManager
+156 −8
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.bluetooth.BluetoothLeBroadcastMetadata
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.media.MediaRoute2Info
import android.media.MediaRouter2Manager
import android.media.RoutingSessionInfo
import android.media.session.MediaController
@@ -34,15 +35,18 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
import com.android.systemui.res.R
import com.android.settingslib.media.PhoneMediaDevice
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.player.MediaDeviceData
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq
@@ -95,6 +99,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
    @Mock private lateinit var device: MediaDevice
    @Mock private lateinit var icon: Drawable
    @Mock private lateinit var route: RoutingSessionInfo
    @Mock private lateinit var selectedRoute: MediaRoute2Info
    @Mock private lateinit var controller: MediaController
    @Mock private lateinit var playbackInfo: PlaybackInfo
    @Mock private lateinit var configurationController: ConfigurationController
@@ -107,6 +112,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
    private lateinit var session: MediaSession
    private lateinit var mediaData: MediaData
    @JvmField @Rule val mockito = MockitoJUnit.rule()
    private val featureFlags = FakeFeatureFlagsClassic()

    @Before
    fun setUp() {
@@ -124,7 +130,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
                localBluetoothManager,
                fakeFgExecutor,
                fakeBgExecutor,
                dumpster
                dumpster,
                featureFlags,
            )
        manager.addListener(listener)

@@ -143,6 +150,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
            MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, token = session.sessionToken)
        whenever(controllerFactory.create(session.sessionToken)).thenReturn(controller)
        setupLeAudioConfiguration(false)
        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, false)
    }

    @After
@@ -454,9 +462,54 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
    }

    @Test
    fun mr2ReturnsRouteWithNullName_useLocalDeviceName() {
    fun mr2ReturnsSystemRouteWithNullName_isPhone_usePhoneName() {
        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
        // When the routing session name is null, and is a system session for a PhoneMediaDevice
        val phoneDevice = mock(PhoneMediaDevice::class.java)
        whenever(phoneDevice.iconWithoutBackground).thenReturn(icon)
        whenever(lmm.currentConnectedDevice).thenReturn(phoneDevice)
        whenever(route.isSystemSession).thenReturn(true)

        whenever(route.name).thenReturn(null)
        whenever(mr2.getSelectedRoutes(any())).thenReturn(listOf(selectedRoute))
        whenever(selectedRoute.name).thenReturn(REMOTE_DEVICE_NAME)
        whenever(selectedRoute.type).thenReturn(MediaRoute2Info.TYPE_BUILTIN_SPEAKER)

        manager.onMediaDataLoaded(KEY, null, mediaData)
        fakeBgExecutor.runAllReady()
        fakeFgExecutor.runAllReady()

        // Then the device name is the PhoneMediaDevice string
        val data = captureDeviceData(KEY)
        assertThat(data.name)
            .isEqualTo(
                context.getString(com.android.settingslib.R.string.media_transfer_this_device_name)
            )
    }

    @Test
    fun mr2ReturnsSystemRouteWithNullName_useSelectedRouteName() {
        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
        // When the routing session does not have a name, and is a system session
        whenever(route.name).thenReturn(null)
        whenever(mr2.getSelectedRoutes(any())).thenReturn(listOf(selectedRoute))
        whenever(selectedRoute.name).thenReturn(REMOTE_DEVICE_NAME)
        whenever(route.isSystemSession).thenReturn(true)

        manager.onMediaDataLoaded(KEY, null, mediaData)
        fakeBgExecutor.runAllReady()
        fakeFgExecutor.runAllReady()

        // Then the device name is the selected route name
        val data = captureDeviceData(KEY)
        assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME)
    }

    @Test
    fun mr2ReturnsNonSystemRouteWithNullName_useLocalDeviceName() {
        // GIVEN that MR2Manager returns a routing session that does not have a name
        whenever(route.name).thenReturn(null)
        whenever(route.isSystemSession).thenReturn(false)
        // WHEN a notification is added
        manager.onMediaDataLoaded(KEY, null, mediaData)
        fakeBgExecutor.runAllReady()
@@ -672,13 +725,108 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
        assertThat(data.showBroadcastButton).isFalse()
    }

    fun captureCallback(): LocalMediaManager.DeviceCallback {
    // Duplicates of above tests with MEDIA_DEVICE_NAME_FIX enabled

    @Test
    fun loadMediaDataWithNullToken_withNameFix() {
        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
        manager.onMediaDataLoaded(KEY, null, mediaData.copy(token = null))
        fakeBgExecutor.runAllReady()
        fakeFgExecutor.runAllReady()
        val data = captureDeviceData(KEY)
        assertThat(data.enabled).isTrue()
        assertThat(data.name).isEqualTo(DEVICE_NAME)
    }

    @Test
    fun onAboutToConnectDeviceAdded_findsDeviceInfoFromAddress_withNameFix() {
        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
        manager.onMediaDataLoaded(KEY, null, mediaData)
        // Run and reset the executors and listeners so we only focus on new events.
        fakeBgExecutor.runAllReady()
        fakeFgExecutor.runAllReady()
        reset(listener)

        // Ensure we'll get device info when using the address
        val fullMediaDevice = mock(MediaDevice::class.java)
        val address = "fakeAddress"
        val nameFromDevice = "nameFromDevice"
        val iconFromDevice = mock(Drawable::class.java)
        whenever(lmm.getMediaDeviceById(eq(address))).thenReturn(fullMediaDevice)
        whenever(fullMediaDevice.name).thenReturn(nameFromDevice)
        whenever(fullMediaDevice.iconWithoutBackground).thenReturn(iconFromDevice)

        // WHEN the about-to-connect device changes to non-null
        val deviceCallback = captureCallback()
        val nameFromParam = "nameFromParam"
        val iconFromParam = mock(Drawable::class.java)
        deviceCallback.onAboutToConnectDeviceAdded(address, nameFromParam, iconFromParam)
        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)

        // THEN the about-to-connect device based on the address is returned
        val data = captureDeviceData(KEY)
        assertThat(data.enabled).isTrue()
        assertThat(data.name).isEqualTo(nameFromDevice)
        assertThat(data.name).isNotEqualTo(nameFromParam)
        assertThat(data.icon).isEqualTo(iconFromDevice)
        assertThat(data.icon).isNotEqualTo(iconFromParam)
    }

    @Test
    fun deviceNameFromMR2RouteInfo_withNameFix() {
        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
        // GIVEN that MR2Manager returns a valid routing session
        whenever(route.name).thenReturn(REMOTE_DEVICE_NAME)
        // WHEN a notification is added
        manager.onMediaDataLoaded(KEY, null, mediaData)
        fakeBgExecutor.runAllReady()
        fakeFgExecutor.runAllReady()
        // THEN it uses the route name (instead of device name)
        val data = captureDeviceData(KEY)
        assertThat(data.enabled).isTrue()
        assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME)
    }

    @Test
    fun deviceDisabledWhenMR2ReturnsNullRouteInfo_withNameFix() {
        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
        // GIVEN that MR2Manager returns null for routing session
        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
        // WHEN a notification is added
        manager.onMediaDataLoaded(KEY, null, mediaData)
        fakeBgExecutor.runAllReady()
        fakeFgExecutor.runAllReady()
        // THEN the device is disabled and name is set to null
        val data = captureDeviceData(KEY)
        assertThat(data.enabled).isFalse()
        assertThat(data.name).isNull()
    }

    @Test
    fun mr2ReturnsNonSystemRouteWithNullName_useLocalDeviceName_withNameFix() {
        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
        // GIVEN that MR2Manager returns a routing session that does not have a name
        whenever(route.name).thenReturn(null)
        whenever(route.isSystemSession).thenReturn(false)
        // WHEN a notification is added
        manager.onMediaDataLoaded(KEY, null, mediaData)
        fakeBgExecutor.runAllReady()
        fakeFgExecutor.runAllReady()
        // THEN the device is enabled and uses the current connected device name
        val data = captureDeviceData(KEY)
        assertThat(data.name).isEqualTo(DEVICE_NAME)
        assertThat(data.enabled).isTrue()
    }

    // End duplicate tests

    private fun captureCallback(): LocalMediaManager.DeviceCallback {
        val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java)
        verify(lmm).registerCallback(captor.capture())
        return captor.getValue()
    }

    fun setupBroadcastCallback(): BluetoothLeBroadcast.Callback {
    private fun setupBroadcastCallback(): BluetoothLeBroadcast.Callback {
        val callback: BluetoothLeBroadcast.Callback =
            object : BluetoothLeBroadcast.Callback {
                override fun onBroadcastStarted(reason: Int, broadcastId: Int) {}
@@ -699,7 +847,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
        return callback
    }

    fun setupLeAudioConfiguration(isLeAudio: Boolean) {
    private fun setupLeAudioConfiguration(isLeAudio: Boolean) {
        whenever(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager)
        whenever(localBluetoothProfileManager.leAudioBroadcastProfile)
            .thenReturn(localBluetoothLeBroadcast)
@@ -707,7 +855,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
        whenever(localBluetoothLeBroadcast.appSourceName).thenReturn(BROADCAST_APP_NAME)
    }

    fun setupBroadcastPackage(currentName: String) {
    private fun setupBroadcastPackage(currentName: String) {
        whenever(lmm.packageName).thenReturn(PACKAGE)
        whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt()))
            .thenReturn(applicationInfo)
@@ -715,7 +863,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
        context.setMockPackageManager(packageManager)
    }

    fun captureDeviceData(key: String, oldKey: String? = null): MediaDeviceData {
    private fun captureDeviceData(key: String, oldKey: String? = null): MediaDeviceData {
        val captor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
        verify(listener).onMediaDeviceChanged(eq(key), eq(oldKey), captor.capture())
        return captor.getValue()