From 1400c7a35f786d9015256b910ecfa2fd89346d52 Mon Sep 17 00:00:00 2001 From: Caitlin Cassidy Date: Mon, 7 Feb 2022 21:50:21 +0000 Subject: [PATCH] [Media SASS] Choose the correct device icon based on the device attributes. Bug: 206614671 Test: verify `adb shell cmd statusbar media-mute-await 9 MuteAwaitDevice start` shows the headphone icon Test: verify `adb shell cmd statusbar media-mute-await 2 MuteAwaitDevice start` shows the smartphone icon Test: atest DeviceIconUtilTest, PhoneMediaDeviceTest Test: atest MediaMuteAwaitConnectionManagerTest Change-Id: I7122828bd65a730b94d20d553509886fd166dcb2 --- .../settingslib/media/DeviceIconUtil.java | 126 +++++++++++++++ .../settingslib/media/PhoneMediaDevice.java | 22 +-- .../settingslib/media/DeviceIconUtilTest.java | 150 ++++++++++++++++++ .../muteawait/MediaMuteAwaitConnectionCli.kt | 11 +- .../MediaMuteAwaitConnectionManager.kt | 16 +- .../MediaMuteAwaitConnectionManagerFactory.kt | 7 +- .../MediaMuteAwaitConnectionManagerTest.kt | 16 +- 7 files changed, 313 insertions(+), 35 deletions(-) create mode 100644 packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java create mode 100644 packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java diff --git a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java new file mode 100644 index 0000000000000..6c0eab3fb0168 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 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.settingslib.media; + +import android.annotation.DrawableRes; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.media.AudioDeviceInfo; +import android.media.MediaRoute2Info; + +import com.android.settingslib.R; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** A util class to get the appropriate icon for different device types. */ +public class DeviceIconUtil { + // A map from a @AudioDeviceInfo.AudioDeviceType to full device information. + private final Map mAudioDeviceTypeToIconMap = new HashMap<>(); + // A map from a @MediaRoute2Info.Type to full device information. + private final Map mMediaRouteTypeToIconMap = new HashMap<>(); + // A default icon to use if the type is not present in the map. + @DrawableRes private static final int DEFAULT_ICON = R.drawable.ic_smartphone; + + public DeviceIconUtil() { + List deviceList = Arrays.asList( + new Device( + AudioDeviceInfo.TYPE_USB_DEVICE, + MediaRoute2Info.TYPE_USB_DEVICE, + R.drawable.ic_headphone), + new Device( + AudioDeviceInfo.TYPE_USB_HEADSET, + MediaRoute2Info.TYPE_USB_HEADSET, + R.drawable.ic_headphone), + new Device( + AudioDeviceInfo.TYPE_USB_ACCESSORY, + MediaRoute2Info.TYPE_USB_ACCESSORY, + R.drawable.ic_headphone), + new Device( + AudioDeviceInfo.TYPE_DOCK, + MediaRoute2Info.TYPE_DOCK, + R.drawable.ic_headphone), + new Device( + AudioDeviceInfo.TYPE_HDMI, + MediaRoute2Info.TYPE_HDMI, + R.drawable.ic_headphone), + new Device( + AudioDeviceInfo.TYPE_WIRED_HEADSET, + MediaRoute2Info.TYPE_WIRED_HEADSET, + R.drawable.ic_headphone), + new Device( + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, + MediaRoute2Info.TYPE_WIRED_HEADPHONES, + R.drawable.ic_headphone), + new Device( + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, + MediaRoute2Info.TYPE_BUILTIN_SPEAKER, + R.drawable.ic_smartphone)); + for (int i = 0; i < deviceList.size(); i++) { + Device device = deviceList.get(i); + mAudioDeviceTypeToIconMap.put(device.mAudioDeviceType, device); + mMediaRouteTypeToIconMap.put(device.mMediaRouteType, device); + } + } + + /** Returns a drawable for an icon representing the given audioDeviceType. */ + public Drawable getIconFromAudioDeviceType( + @AudioDeviceInfo.AudioDeviceType int audioDeviceType, Context context) { + return context.getDrawable(getIconResIdFromAudioDeviceType(audioDeviceType)); + } + + /** Returns a drawable res ID for an icon representing the given audioDeviceType. */ + @DrawableRes + public int getIconResIdFromAudioDeviceType( + @AudioDeviceInfo.AudioDeviceType int audioDeviceType) { + if (mAudioDeviceTypeToIconMap.containsKey(audioDeviceType)) { + return mAudioDeviceTypeToIconMap.get(audioDeviceType).mIconDrawableRes; + } + return DEFAULT_ICON; + } + + /** Returns a drawable res ID for an icon representing the given mediaRouteType. */ + @DrawableRes + public int getIconResIdFromMediaRouteType( + @MediaRoute2Info.Type int mediaRouteType) { + if (mMediaRouteTypeToIconMap.containsKey(mediaRouteType)) { + return mMediaRouteTypeToIconMap.get(mediaRouteType).mIconDrawableRes; + } + return DEFAULT_ICON; + } + + private static class Device { + @AudioDeviceInfo.AudioDeviceType + private final int mAudioDeviceType; + + @MediaRoute2Info.Type + private final int mMediaRouteType; + + @DrawableRes + private final int mIconDrawableRes; + + Device(@AudioDeviceInfo.AudioDeviceType int audioDeviceType, + @MediaRoute2Info.Type int mediaRouteType, + @DrawableRes int iconDrawableRes) { + mAudioDeviceType = audioDeviceType; + mMediaRouteType = mediaRouteType; + mIconDrawableRes = iconDrawableRes; + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index c16ecb5587125..921c245264458 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -47,10 +47,12 @@ public class PhoneMediaDevice extends MediaDevice { private String mSummary = ""; + private final DeviceIconUtil mDeviceIconUtil; + PhoneMediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, String packageName) { super(context, routerManager, info, packageName); - + mDeviceIconUtil = new DeviceIconUtil(); initDeviceRecord(); } @@ -94,23 +96,7 @@ public class PhoneMediaDevice extends MediaDevice { @VisibleForTesting int getDrawableResId() { - int resId; - switch (mRouteInfo.getType()) { - case TYPE_USB_DEVICE: - case TYPE_USB_HEADSET: - case TYPE_USB_ACCESSORY: - case TYPE_DOCK: - case TYPE_HDMI: - case TYPE_WIRED_HEADSET: - case TYPE_WIRED_HEADPHONES: - resId = R.drawable.ic_headphone; - break; - case TYPE_BUILTIN_SPEAKER: - default: - resId = R.drawable.ic_smartphone; - break; - } - return resId; + return mDeviceIconUtil.getIconResIdFromMediaRouteType(mRouteInfo.getType()); } @Override diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java new file mode 100644 index 0000000000000..72dfc17332750 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2022 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.settingslib.media; + +import static com.google.common.truth.Truth.assertThat; + +import android.media.AudioDeviceInfo; +import android.media.MediaRoute2Info; + +import com.android.settingslib.R; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class DeviceIconUtilTest { + private final DeviceIconUtil mDeviceIconUtil = new DeviceIconUtil(); + + @Test + public void getIconResIdFromMediaRouteType_usbDevice_isHeadphone() { + assertThat(mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_DEVICE)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_usbHeadset_isHeadphone() { + assertThat(mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_HEADSET)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_usbAccessory_isHeadphone() { + assertThat( + mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_ACCESSORY)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_dock_isHeadphone() { + assertThat(mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_DOCK)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_hdmi_isHeadphone() { + assertThat(mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_HDMI)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_wiredHeadset_isHeadphone() { + assertThat( + mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_WIRED_HEADSET)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_wiredHeadphones_isHeadphone() { + assertThat( + mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_WIRED_HEADPHONES)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_builtinSpeaker_isSmartphone() { + assertThat( + mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER)) + .isEqualTo(R.drawable.ic_smartphone); + } + + @Test + public void getIconResIdFromMediaRouteType_unsupportedType_isSmartphone() { + assertThat(mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_UNKNOWN)) + .isEqualTo(R.drawable.ic_smartphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_usbDevice_isHeadphone() { + assertThat(mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_DEVICE)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_usbHeadset_isHeadphone() { + assertThat( + mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_HEADSET)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_usbAccessory_isHeadphone() { + assertThat( + mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_ACCESSORY)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_dock_isHeadphone() { + assertThat(mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_DOCK)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_hdmi_isHeadphone() { + assertThat(mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_HDMI)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_wiredHeadset_isHeadphone() { + assertThat( + mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_WIRED_HEADSET)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_wiredHeadphones_isHeadphone() { + assertThat( + mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_WIRED_HEADPHONES)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_builtinSpeaker_isSmartphone() { + assertThat( + mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) + .isEqualTo(R.drawable.ic_smartphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_unsupportedType_isSmartphone() { + assertThat(mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_UNKNOWN)) + .isEqualTo(R.drawable.ic_smartphone); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt index af072cbfded5c..2ae3a631fa5a1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt @@ -19,7 +19,6 @@ package com.android.systemui.media.muteawait import android.content.Context import android.media.AudioAttributes.USAGE_MEDIA import android.media.AudioDeviceAttributes -import android.media.AudioDeviceInfo import android.media.AudioManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.commandline.Command @@ -42,13 +41,13 @@ class MediaMuteAwaitConnectionCli @Inject constructor( override fun execute(pw: PrintWriter, args: List) { val device = AudioDeviceAttributes( AudioDeviceAttributes.ROLE_OUTPUT, - AudioDeviceInfo.TYPE_USB_HEADSET, + /* type= */ Integer.parseInt(args[0]), ADDRESS, - /* name= */ args[0], + /* name= */ args[1], listOf(), listOf(), ) - val startOrCancel = args[1] + val startOrCancel = args[2] val audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager @@ -58,12 +57,12 @@ class MediaMuteAwaitConnectionCli @Inject constructor( intArrayOf(USAGE_MEDIA), device, TIMEOUT, TIMEOUT_UNITS ) CANCEL -> audioManager.cancelMuteAwaitConnection(device) - else -> pw.println("Must specify $START or $CANCEL") + else -> pw.println("Must specify `$START` or `$CANCEL`; was $startOrCancel") } } override fun help(pw: PrintWriter) { pw.println("Usage: adb shell cmd statusbar $MEDIA_MUTE_AWAIT_COMMAND " + - "[name] [$START|$CANCEL]") + "[type] [name] [$START|$CANCEL]") } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt index 895fcf046f4dd..1b53776e045bb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt @@ -21,8 +21,8 @@ import android.graphics.drawable.Drawable import android.media.AudioAttributes.USAGE_MEDIA import android.media.AudioDeviceAttributes import android.media.AudioManager +import com.android.settingslib.media.DeviceIconUtil import com.android.settingslib.media.LocalMediaManager -import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Main import java.util.concurrent.Executor @@ -39,7 +39,8 @@ import java.util.concurrent.Executor class MediaMuteAwaitConnectionManager constructor( @Main mainExecutor: Executor, localMediaManager: LocalMediaManager, - private val context: Context + private val context: Context, + private val deviceIconUtil: DeviceIconUtil ) { var currentMutedDevice: AudioDeviceAttributes? = null @@ -51,7 +52,7 @@ class MediaMuteAwaitConnectionManager constructor( // There should only be one device that's mutedUntilConnection at a time, so we can // safely override any previous value. currentMutedDevice = device - localMediaManager.dispatchAboutToConnectDeviceChanged(device.name, getIcon()) + localMediaManager.dispatchAboutToConnectDeviceChanged(device.name, device.getIcon()) } } @@ -75,12 +76,13 @@ class MediaMuteAwaitConnectionManager constructor( val currentDevice = audioManager.mutingExpectedDevice if (currentDevice != null) { currentMutedDevice = currentDevice - localMediaManager.dispatchAboutToConnectDeviceChanged(currentDevice.name, getIcon()) + localMediaManager.dispatchAboutToConnectDeviceChanged( + currentDevice.name, currentDevice.getIcon() + ) } } - private fun getIcon(): Drawable { - // TODO(b/206614671): Choose the icon based on device type. - return context.getDrawable(R.drawable.ic_headphone)!! + private fun AudioDeviceAttributes.getIcon(): Drawable { + return deviceIconUtil.getIconFromAudioDeviceType(this.type, context) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt index 2c13ab4b369e0..118b2dd4dc90d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt @@ -17,6 +17,7 @@ package com.android.systemui.media.muteawait import android.content.Context +import com.android.settingslib.media.DeviceIconUtil import com.android.settingslib.media.LocalMediaManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main @@ -31,11 +32,15 @@ class MediaMuteAwaitConnectionManagerFactory @Inject constructor( private val context: Context, @Main private val mainExecutor: Executor ) { + private val deviceIconUtil = DeviceIconUtil() + /** Creates a [MediaMuteAwaitConnectionManager]. */ fun create(localMediaManager: LocalMediaManager): MediaMuteAwaitConnectionManager? { if (!mediaFlags.areMuteAwaitConnectionsEnabled()) { return null } - return MediaMuteAwaitConnectionManager(mainExecutor, localMediaManager, context) + return MediaMuteAwaitConnectionManager( + mainExecutor, localMediaManager, context, deviceIconUtil + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt index 0b89f327bea15..98d5cbacb253a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt @@ -16,7 +16,9 @@ package com.android.systemui.media.muteawait +import android.annotation.DrawableRes import android.content.Context +import android.graphics.drawable.Drawable import android.media.AudioAttributes.USAGE_MEDIA import android.media.AudioAttributes.USAGE_UNKNOWN import android.media.AudioDeviceAttributes @@ -24,7 +26,9 @@ import android.media.AudioDeviceInfo import android.media.AudioManager import android.media.AudioManager.MuteAwaitConnectionCallback.EVENT_CONNECTION import android.test.suitebuilder.annotation.SmallTest +import com.android.settingslib.media.DeviceIconUtil import com.android.settingslib.media.LocalMediaManager +import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -47,13 +51,18 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { @Mock private lateinit var audioManager: AudioManager @Mock + private lateinit var deviceIconUtil: DeviceIconUtil + @Mock private lateinit var localMediaManager: LocalMediaManager private lateinit var muteAwaitListener: AudioManager.MuteAwaitConnectionCallback + private lateinit var icon: Drawable @Before fun setUp() { MockitoAnnotations.initMocks(this) context.addMockSystemService(Context.AUDIO_SERVICE, audioManager) + icon = context.getDrawable(R.drawable.ic_cake)!! + whenever(deviceIconUtil.getIconFromAudioDeviceType(any(), any())).thenReturn(icon) } @Test @@ -71,7 +80,7 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { instantiateManager() - verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(DEVICE_NAME), any()) + verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(DEVICE_NAME), eq(icon)) } @Test @@ -89,7 +98,7 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA)) - verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(DEVICE_NAME), any()) + verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(DEVICE_NAME), eq(icon)) } @Test @@ -146,7 +155,8 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { muteAwaitConnectionManager = MediaMuteAwaitConnectionManager( FakeExecutor(FakeSystemClock()), localMediaManager, - context + context, + deviceIconUtil ) val listenerCaptor = ArgumentCaptor.forClass(