From bc8c6790931b8a5172b67225dc81f552ee90fb50 Mon Sep 17 00:00:00 2001 From: Caitlin Cassidy Date: Thu, 3 Feb 2022 18:02:48 +0000 Subject: [PATCH 1/3] [Media SASS] Display about-to-connect device names in the media player device chip. Bug: 206614671 Test: atest MediaMuteAwaitConnectionManagerTest Test: manual: `adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id 904 --ez value 1` to flip the flag then `adb shell cmd statusbar media-mute-await MuteAwaitDevice start` and verify that the media player updates to show "MuteAwaitDevice" as the device with a headphone icon. Video uploaded to bug showing this interaction. Change-Id: I4b3dde002904026334f03068dc3574d4b3c5fede --- .../settingslib/media/LocalMediaManager.java | 28 +++ .../systemui/dagger/SysUIComponent.java | 5 + .../src/com/android/systemui/flags/Flags.java | 1 + .../systemui/media/MediaDeviceManager.kt | 31 +++- .../com/android/systemui/media/MediaFlags.kt | 7 +- .../systemui/media/dagger/MediaModule.java | 16 ++ .../muteawait/MediaMuteAwaitConnectionCli.kt | 76 ++++++++ .../MediaMuteAwaitConnectionManager.kt | 86 +++++++++ .../MediaMuteAwaitConnectionManagerFactory.kt | 41 +++++ .../systemui/media/MediaDeviceManagerTest.kt | 59 +++++- .../MediaMuteAwaitConnectionManagerTest.kt | 168 ++++++++++++++++++ 11 files changed, 513 insertions(+), 5 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt create mode 100644 packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index 865c2f0bc836a..66365b64b179f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -21,6 +21,7 @@ import android.app.Notification; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; +import android.graphics.drawable.Drawable; import android.media.RoutingSessionInfo; import android.os.Build; import android.text.TextUtils; @@ -226,6 +227,18 @@ public class LocalMediaManager implements BluetoothCallback { } } + /** + * Dispatch a change in the about-to-connect device. See + * {@link DeviceCallback#onAboutToConnectDeviceChanged} for more information. + */ + public void dispatchAboutToConnectDeviceChanged( + @Nullable String deviceName, + @Nullable Drawable deviceIcon) { + for (DeviceCallback callback : getCallbacks()) { + callback.onAboutToConnectDeviceChanged(deviceName, deviceIcon); + } + } + /** * Stop scan MediaDevice */ @@ -674,6 +687,21 @@ public class LocalMediaManager implements BluetoothCallback { * {@link android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND}, */ default void onRequestFailed(int reason){}; + + /** + * Callback for notifying that we have a new about-to-connect device. + * + * An about-to-connect device is a device that is not yet connected but is expected to + * connect imminently and should be displayed as the current device in the media player. + * See [AudioManager.muteAwaitConnection] for more details. + * + * @param deviceName the name of the device (displayed to the user). + * @param deviceIcon the icon that should be used with the device. + */ + default void onAboutToConnectDeviceChanged( + @Nullable String deviceName, + @Nullable Drawable deviceIcon + ) {} } /** diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index bda8e3c2ed631..cf0cbee552507 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -23,6 +23,7 @@ import com.android.systemui.InitController; import com.android.systemui.SystemUIAppComponentFactory; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardSliceProvider; +import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli; import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper; import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver; import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender; @@ -144,6 +145,7 @@ public interface SysUIComponent { getMediaTttChipControllerSender(); getMediaTttChipControllerReceiver(); getMediaTttCommandLineHelper(); + getMediaMuteAwaitConnectionCli(); getUnfoldLatencyTracker().init(); getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init); getFoldStateLogger().ifPresent(FoldStateLogger::init); @@ -220,6 +222,9 @@ public interface SysUIComponent { /** */ Optional getMediaTttCommandLineHelper(); + /** */ + Optional getMediaMuteAwaitConnectionCli(); + /** * Member injection into the supplied argument. */ diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index e1f8f0718077a..2d6f9b1f44204 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -138,6 +138,7 @@ public class Flags { public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, false); public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, true); public static final BooleanFlag MEDIA_SESSION_LAYOUT = new BooleanFlag(902, false); + public static final BooleanFlag MEDIA_MUTE_AWAIT = new BooleanFlag(904, true); // Pay no attention to the reflection behind the curtain. // ========================== Curtain ========================== diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt index bed254fe82497..a7bd68b5450d3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt @@ -16,6 +16,7 @@ package com.android.systemui.media +import android.graphics.drawable.Drawable import android.media.MediaRouter2Manager import android.media.session.MediaController import androidx.annotation.AnyThread @@ -27,6 +28,7 @@ import com.android.systemui.Dumpable import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory import java.io.FileDescriptor import java.io.PrintWriter import java.util.concurrent.Executor @@ -41,6 +43,7 @@ class MediaDeviceManager @Inject constructor( private val controllerFactory: MediaControllerFactory, private val localMediaManagerFactory: LocalMediaManagerFactory, private val mr2manager: MediaRouter2Manager, + private val muteAwaitConnectionManagerFactory: MediaMuteAwaitConnectionManagerFactory, @Main private val fgExecutor: Executor, @Background private val bgExecutor: Executor, dumpManager: DumpManager @@ -80,8 +83,16 @@ class MediaDeviceManager @Inject constructor( val controller = data.token?.let { controllerFactory.create(it) } - entry = Entry(key, oldKey, controller, - localMediaManagerFactory.create(data.packageName)) + val localMediaManager = localMediaManagerFactory.create(data.packageName) + // We don't need to set this muteAwaitConnectionManager anywhere; it will just notify + // [localMediaManager] on the appropriate events. + muteAwaitConnectionManagerFactory.create(localMediaManager) + entry = Entry( + key, + oldKey, + controller, + localMediaManager + ) entries[key] = entry entry.start() } @@ -142,6 +153,9 @@ class MediaDeviceManager @Inject constructor( } } } + // A device that is not yet connected but is expected to connect imminently. Because it's + // expected to connect imminently, it should be displayed as the current device. + private var aboutToConnectDeviceOverride: MediaDeviceData? = null @AnyThread fun start() = bgExecutor.execute { @@ -197,8 +211,21 @@ class MediaDeviceManager @Inject constructor( } } + override fun onAboutToConnectDeviceChanged(deviceName: String?, deviceIcon: Drawable?) { + aboutToConnectDeviceOverride = if (deviceName == null || deviceIcon == null) { + null + } else { + MediaDeviceData(enabled = true, deviceIcon, deviceName) + } + updateCurrent() + } + @WorkerThread private fun updateCurrent() { + if (aboutToConnectDeviceOverride != null) { + current = aboutToConnectDeviceOverride + return + } val device = localMediaManager.currentConnectedDevice val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt index b9795f1265fa3..e1467683c986c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt @@ -37,4 +37,9 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { return featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS) && featureFlags.isEnabled(Flags.MEDIA_SESSION_LAYOUT) } -} \ No newline at end of file + + /** + * Check whether we support displaying information about mute await connections. + */ + fun areMuteAwaitConnectionsEnabled() = featureFlags.isEnabled(Flags.MEDIA_MUTE_AWAIT) +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index f8b34f9769e42..f9333a3e157d9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -23,10 +23,12 @@ import android.view.WindowManager; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.media.MediaDataManager; +import com.android.systemui.media.MediaFlags; import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.media.MediaHost; import com.android.systemui.media.MediaHostStatesManager; import com.android.systemui.media.dream.dagger.MediaComplicationComponent; +import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli; import com.android.systemui.media.nearby.NearbyMediaDevicesService; import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper; import com.android.systemui.media.taptotransfer.MediaTttFlags; @@ -140,6 +142,20 @@ public interface MediaModule { new MediaTttCommandLineHelper(commandRegistry, context, mainExecutor)); } + /** */ + @Provides + @SysUISingleton + static Optional providesMediaMuteAwaitConnectionCli( + MediaFlags mediaFlags, + CommandRegistry commandRegistry, + Context context + ) { + if (!mediaFlags.areMuteAwaitConnectionsEnabled()) { + return Optional.empty(); + } + return Optional.of(new MediaMuteAwaitConnectionCli(commandRegistry, context)); + } + /** Inject into NearbyMediaDevicesService. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt new file mode 100644 index 0000000000000..af072cbfded5c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt @@ -0,0 +1,76 @@ +/* + * 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.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 +import com.android.systemui.statusbar.commandline.CommandRegistry +import java.io.PrintWriter +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** A command line interface to manually test [MediaMuteAwaitConnectionManager]. */ +@SysUISingleton +class MediaMuteAwaitConnectionCli @Inject constructor( + commandRegistry: CommandRegistry, + private val context: Context +) { + init { + commandRegistry.registerCommand(MEDIA_MUTE_AWAIT_COMMAND) { MuteAwaitCommand() } + } + + inner class MuteAwaitCommand : Command { + override fun execute(pw: PrintWriter, args: List) { + val device = AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_USB_HEADSET, + ADDRESS, + /* name= */ args[0], + listOf(), + listOf(), + ) + val startOrCancel = args[1] + + val audioManager: AudioManager = + context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + when (startOrCancel) { + START -> + audioManager.muteAwaitConnection( + intArrayOf(USAGE_MEDIA), device, TIMEOUT, TIMEOUT_UNITS + ) + CANCEL -> audioManager.cancelMuteAwaitConnection(device) + else -> pw.println("Must specify $START or $CANCEL") + } + } + override fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar $MEDIA_MUTE_AWAIT_COMMAND " + + "[name] [$START|$CANCEL]") + } + } +} + +private const val MEDIA_MUTE_AWAIT_COMMAND = "media-mute-await" +private const val START = "start" +private const val CANCEL = "cancel" +private const val ADDRESS = "address" +private const val TIMEOUT = 5L +private val TIMEOUT_UNITS = TimeUnit.SECONDS diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt new file mode 100644 index 0000000000000..895fcf046f4dd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt @@ -0,0 +1,86 @@ +/* + * 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.systemui.media.muteawait + +import android.content.Context +import android.graphics.drawable.Drawable +import android.media.AudioAttributes.USAGE_MEDIA +import android.media.AudioDeviceAttributes +import android.media.AudioManager +import com.android.settingslib.media.LocalMediaManager +import com.android.systemui.R +import com.android.systemui.dagger.qualifiers.Main +import java.util.concurrent.Executor + +/** + * A class responsible for keeping track of devices that have muted audio playback until the device + * is connected. The device connection expected to happen imminently, so we'd like to display the + * device name in the media player. When the about-to-connect device changes, [localMediaManager] + * will be notified. + * + * See [AudioManager.muteAwaitConnection] and b/206614671 for more details. + * + * TODO(b/206614671): Add logging. + */ +class MediaMuteAwaitConnectionManager constructor( + @Main mainExecutor: Executor, + localMediaManager: LocalMediaManager, + private val context: Context +) { + var currentMutedDevice: AudioDeviceAttributes? = null + + val audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + val muteAwaitConnectionChangeListener = object : AudioManager.MuteAwaitConnectionCallback() { + override fun onMutedUntilConnection(device: AudioDeviceAttributes, mutedUsages: IntArray) { + if (USAGE_MEDIA in mutedUsages) { + // 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()) + } + } + + override fun onUnmutedEvent( + @UnmuteEvent unmuteEvent: Int, + device: AudioDeviceAttributes, + mutedUsages: IntArray + ) { + if (currentMutedDevice == device && USAGE_MEDIA in mutedUsages) { + currentMutedDevice = null + localMediaManager.dispatchAboutToConnectDeviceChanged(null, null) + } + } + } + + init { + // TODO(b/206614671): Unregister this callback (likely on [MediaDeviceManager.Entry.Stop]). + audioManager.registerMuteAwaitConnectionCallback( + mainExecutor, muteAwaitConnectionChangeListener + ) + val currentDevice = audioManager.mutingExpectedDevice + if (currentDevice != null) { + currentMutedDevice = currentDevice + localMediaManager.dispatchAboutToConnectDeviceChanged(currentDevice.name, getIcon()) + } + } + + private fun getIcon(): Drawable { + // TODO(b/206614671): Choose the icon based on device type. + return context.getDrawable(R.drawable.ic_headphone)!! + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt new file mode 100644 index 0000000000000..2c13ab4b369e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt @@ -0,0 +1,41 @@ +/* + * 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.systemui.media.muteawait + +import android.content.Context +import com.android.settingslib.media.LocalMediaManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.MediaFlags +import java.util.concurrent.Executor +import javax.inject.Inject + +/** Factory class to create [MediaMuteAwaitConnectionManager] instances. */ +@SysUISingleton +class MediaMuteAwaitConnectionManagerFactory @Inject constructor( + private val mediaFlags: MediaFlags, + private val context: Context, + @Main private val mainExecutor: Executor +) { + /** Creates a [MediaMuteAwaitConnectionManager]. */ + fun create(localMediaManager: LocalMediaManager): MediaMuteAwaitConnectionManager? { + if (!mediaFlags.areMuteAwaitConnectionsEnabled()) { + return null + } + return MediaMuteAwaitConnectionManager(mainExecutor, localMediaManager, context) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt index 3d59497fd978e..e28927e3aac23 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt @@ -30,6 +30,7 @@ import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock @@ -44,6 +45,7 @@ import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.any +import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify @@ -71,6 +73,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Mock private lateinit var lmmFactory: LocalMediaManagerFactory @Mock private lateinit var lmm: LocalMediaManager @Mock private lateinit var mr2: MediaRouter2Manager + @Mock private lateinit var muteAwaitFactory: MediaMuteAwaitConnectionManagerFactory private lateinit var fakeFgExecutor: FakeExecutor private lateinit var fakeBgExecutor: FakeExecutor @Mock private lateinit var dumpster: DumpManager @@ -88,8 +91,15 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fun setUp() { fakeFgExecutor = FakeExecutor(FakeSystemClock()) fakeBgExecutor = FakeExecutor(FakeSystemClock()) - manager = MediaDeviceManager(controllerFactory, lmmFactory, mr2, fakeFgExecutor, - fakeBgExecutor, dumpster) + manager = MediaDeviceManager( + controllerFactory, + lmmFactory, + mr2, + muteAwaitFactory, + fakeFgExecutor, + fakeBgExecutor, + dumpster + ) manager.addListener(listener) // Configure mocks. @@ -267,6 +277,51 @@ public class MediaDeviceManagerTest : SysuiTestCase() { assertThat(data.icon).isEqualTo(icon) } + @Test + fun onAboutToConnectDeviceChangedWithNonNullParams() { + 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) + + val deviceCallback = captureCallback() + // WHEN the about-to-connect device changes to non-null + val name = "AboutToConnectDeviceName" + val mockIcon = mock(Drawable::class.java) + deviceCallback.onAboutToConnectDeviceChanged(name, mockIcon) + assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1) + // THEN the about-to-connect device is returned + val data = captureDeviceData(KEY) + assertThat(data.enabled).isTrue() + assertThat(data.name).isEqualTo(name) + assertThat(data.icon).isEqualTo(mockIcon) + } + + @Test + fun onAboutToConnectDeviceChangedWithNullParams() { + manager.onMediaDataLoaded(KEY, null, mediaData) + fakeBgExecutor.runAllReady() + val deviceCallback = captureCallback() + // First set a non-null about-to-connect device + deviceCallback.onAboutToConnectDeviceChanged( + "AboutToConnectDeviceName", mock(Drawable::class.java) + ) + // Run and reset the executors and listeners so we only focus on new events. + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() + reset(listener) + + // WHEN the about-to-connect device changes to null + deviceCallback.onAboutToConnectDeviceChanged(null, null) + assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1) + // THEN the normal device is returned + val data = captureDeviceData(KEY) + assertThat(data.enabled).isTrue() + assertThat(data.name).isEqualTo(DEVICE_NAME) + assertThat(data.icon).isEqualTo(icon) + } + @Test fun listenerReceivesKeyRemoved() { manager.onMediaDataLoaded(KEY, null, mediaData) 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 new file mode 100644 index 0000000000000..0b89f327bea15 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt @@ -0,0 +1,168 @@ +/* + * 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.systemui.media.muteawait + +import android.content.Context +import android.media.AudioAttributes.USAGE_MEDIA +import android.media.AudioAttributes.USAGE_UNKNOWN +import android.media.AudioDeviceAttributes +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.LocalMediaManager +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.time.FakeSystemClock +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + + +@SmallTest +class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { + private lateinit var muteAwaitConnectionManager: MediaMuteAwaitConnectionManager + @Mock + private lateinit var audioManager: AudioManager + @Mock + private lateinit var localMediaManager: LocalMediaManager + private lateinit var muteAwaitListener: AudioManager.MuteAwaitConnectionCallback + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + context.addMockSystemService(Context.AUDIO_SERVICE, audioManager) + } + + @Test + fun constructor_audioManagerHasNoMuteAwaitDevice_localMediaMangerNotNotified() { + whenever(audioManager.mutingExpectedDevice).thenReturn(null) + + instantiateManager() + + verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any()) + } + + @Test + fun constructor_audioManagerHasMuteAwaitDevice_localMediaMangerNotified() { + whenever(audioManager.mutingExpectedDevice).thenReturn(DEVICE) + + instantiateManager() + + verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(DEVICE_NAME), any()) + } + + @Test + fun onMutedUntilConnection_notUsageMedia_localMediaManagerNotNotified() { + instantiateManager() + + muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_UNKNOWN)) + + verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any()) + } + + @Test + fun onMutedUntilConnection_isUsageMedia_localMediaManagerNotified() { + instantiateManager() + + muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA)) + + verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(DEVICE_NAME), any()) + } + + @Test + fun onUnmutedEvent_noDeviceMutedBefore_localMediaManagerNotNotified() { + instantiateManager() + muteAwaitListener.onUnmutedEvent(EVENT_CONNECTION, DEVICE, intArrayOf(USAGE_MEDIA)) + + verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any()) + } + + @Test + fun onUnmutedEvent_notSameDevice_localMediaManagerNotNotified() { + instantiateManager() + muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA)) + reset(localMediaManager) + + val otherDevice = AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_USB_HEADSET, + "address", + "DifferentName", + listOf(), + listOf(), + ) + muteAwaitListener.onUnmutedEvent(EVENT_CONNECTION, otherDevice, intArrayOf(USAGE_MEDIA)) + + verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any()) + } + + @Test + fun onUnmutedEvent_notUsageMedia_localMediaManagerNotNotified() { + instantiateManager() + muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA)) + reset(localMediaManager) + + muteAwaitListener.onUnmutedEvent(EVENT_CONNECTION, DEVICE, intArrayOf(USAGE_UNKNOWN)) + + verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any()) + } + + @Test + fun onUnmutedEvent_sameDeviceAndUsageMedia_localMediaManagerNotified() { + instantiateManager() + muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA)) + reset(localMediaManager) + + muteAwaitListener.onUnmutedEvent(EVENT_CONNECTION, DEVICE, intArrayOf(USAGE_MEDIA)) + + verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(null), eq(null)) + } + + // Some classes test the constructor, so don't instantiate the manager in @SetUp. + private fun instantiateManager() { + muteAwaitConnectionManager = MediaMuteAwaitConnectionManager( + FakeExecutor(FakeSystemClock()), + localMediaManager, + context + ) + + val listenerCaptor = ArgumentCaptor.forClass( + AudioManager.MuteAwaitConnectionCallback::class.java + ) + verify(audioManager).registerMuteAwaitConnectionCallback(any(), listenerCaptor.capture()) + muteAwaitListener = listenerCaptor.value!! + } +} + +private const val DEVICE_NAME = "DeviceName" +private val DEVICE = AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_USB_HEADSET, + "address", + DEVICE_NAME, + listOf(), + listOf(), +) From 1400c7a35f786d9015256b910ecfa2fd89346d52 Mon Sep 17 00:00:00 2001 From: Caitlin Cassidy Date: Mon, 7 Feb 2022 21:50:21 +0000 Subject: [PATCH 2/3] [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( From b56e31385e570e6706d4251222185d4d26fabc33 Mon Sep 17 00:00:00 2001 From: Caitlin Cassidy Date: Wed, 9 Feb 2022 17:18:10 +0000 Subject: [PATCH 3/3] [Media SASS] Unregister the muteAwaitConnectionCallback when the Entry stops. Bug: 206614671 Test: atest MediaMuteAwaitConnectionManagerTest Test: atest MediaDeviceManagerTest Change-Id: I370edb601412af85683fb4d2ff514f35ef44bceb --- .../systemui/media/MediaDeviceManager.kt | 14 ++-- .../MediaMuteAwaitConnectionManager.kt | 13 ++-- .../systemui/media/MediaDeviceManagerTest.kt | 6 ++ .../MediaMuteAwaitConnectionManagerTest.kt | 68 +++++++++++++------ 4 files changed, 70 insertions(+), 31 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt index a7bd68b5450d3..f972da560ed87 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt @@ -28,6 +28,7 @@ import com.android.systemui.Dumpable import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory import java.io.FileDescriptor import java.io.PrintWriter @@ -84,14 +85,14 @@ class MediaDeviceManager @Inject constructor( controllerFactory.create(it) } val localMediaManager = localMediaManagerFactory.create(data.packageName) - // We don't need to set this muteAwaitConnectionManager anywhere; it will just notify - // [localMediaManager] on the appropriate events. - muteAwaitConnectionManagerFactory.create(localMediaManager) + val muteAwaitConnectionManager = + muteAwaitConnectionManagerFactory.create(localMediaManager) entry = Entry( key, oldKey, controller, - localMediaManager + localMediaManager, + muteAwaitConnectionManager ) entries[key] = entry entry.start() @@ -137,7 +138,8 @@ class MediaDeviceManager @Inject constructor( val key: String, val oldKey: String?, val controller: MediaController?, - val localMediaManager: LocalMediaManager + val localMediaManager: LocalMediaManager, + val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager? ) : LocalMediaManager.DeviceCallback, MediaController.Callback() { val token @@ -161,6 +163,7 @@ class MediaDeviceManager @Inject constructor( fun start() = bgExecutor.execute { localMediaManager.registerCallback(this) localMediaManager.startScan() + muteAwaitConnectionManager?.startListening() playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN controller?.registerCallback(this) updateCurrent() @@ -173,6 +176,7 @@ class MediaDeviceManager @Inject constructor( controller?.unregisterCallback(this) localMediaManager.stopScan() localMediaManager.unregisterCallback(this) + muteAwaitConnectionManager?.stopListening() } fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array) { 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 1b53776e045bb..22bc5572f5a5b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt @@ -37,8 +37,8 @@ import java.util.concurrent.Executor * TODO(b/206614671): Add logging. */ class MediaMuteAwaitConnectionManager constructor( - @Main mainExecutor: Executor, - localMediaManager: LocalMediaManager, + @Main private val mainExecutor: Executor, + private val localMediaManager: LocalMediaManager, private val context: Context, private val deviceIconUtil: DeviceIconUtil ) { @@ -68,8 +68,8 @@ class MediaMuteAwaitConnectionManager constructor( } } - init { - // TODO(b/206614671): Unregister this callback (likely on [MediaDeviceManager.Entry.Stop]). + /** Start listening for mute await events. */ + fun startListening() { audioManager.registerMuteAwaitConnectionCallback( mainExecutor, muteAwaitConnectionChangeListener ) @@ -82,6 +82,11 @@ class MediaMuteAwaitConnectionManager constructor( } } + /** Stop listening for mute await events. */ + fun stopListening() { + audioManager.unregisterMuteAwaitConnectionCallback(muteAwaitConnectionChangeListener) + } + private fun AudioDeviceAttributes.getIcon(): Drawable { return deviceIconUtil.getIconFromAudioDeviceType(this.type, context) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt index e28927e3aac23..64b5b86f47f66 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt @@ -30,6 +30,7 @@ import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock @@ -74,6 +75,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Mock private lateinit var lmm: LocalMediaManager @Mock private lateinit var mr2: MediaRouter2Manager @Mock private lateinit var muteAwaitFactory: MediaMuteAwaitConnectionManagerFactory + @Mock private lateinit var muteAwaitManager: MediaMuteAwaitConnectionManager private lateinit var fakeFgExecutor: FakeExecutor private lateinit var fakeBgExecutor: FakeExecutor @Mock private lateinit var dumpster: DumpManager @@ -106,6 +108,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(device.name).thenReturn(DEVICE_NAME) whenever(device.iconWithoutBackground).thenReturn(icon) whenever(lmmFactory.create(PACKAGE)).thenReturn(lmm) + whenever(muteAwaitFactory.create(lmm)).thenReturn(muteAwaitManager) whenever(lmm.getCurrentConnectedDevice()).thenReturn(device) whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(route) @@ -156,6 +159,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { manager.onMediaDataRemoved(KEY) fakeBgExecutor.runAllReady() verify(lmm).unregisterCallback(any()) + verify(muteAwaitManager).stopListening() } @Test @@ -179,6 +183,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fakeFgExecutor.runAllReady() // THEN the listener for the old key should removed. verify(lmm).unregisterCallback(any()) + verify(muteAwaitManager).stopListening() // AND a new device event emitted val data = captureDeviceData(KEY, KEY_OLD) assertThat(data.enabled).isTrue() @@ -250,6 +255,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { manager.onMediaDataLoaded(KEY, null, mediaData) fakeBgExecutor.runAllReady() val deviceCallback = captureCallback() + verify(muteAwaitManager).startListening() // WHEN the device list changes deviceCallback.onDeviceListUpdate(mutableListOf(device)) assertThat(fakeBgExecutor.runAllReady()).isEqualTo(1) 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 98d5cbacb253a..88c451499d21b 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,6 @@ 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 @@ -54,7 +53,6 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { private lateinit var deviceIconUtil: DeviceIconUtil @Mock private lateinit var localMediaManager: LocalMediaManager - private lateinit var muteAwaitListener: AudioManager.MuteAwaitConnectionCallback private lateinit var icon: Drawable @Before @@ -63,29 +61,56 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { context.addMockSystemService(Context.AUDIO_SERVICE, audioManager) icon = context.getDrawable(R.drawable.ic_cake)!! whenever(deviceIconUtil.getIconFromAudioDeviceType(any(), any())).thenReturn(icon) + + muteAwaitConnectionManager = MediaMuteAwaitConnectionManager( + FakeExecutor(FakeSystemClock()), + localMediaManager, + context, + deviceIconUtil + ) } @Test - fun constructor_audioManagerHasNoMuteAwaitDevice_localMediaMangerNotNotified() { + fun constructor_audioManagerCallbackNotRegistered() { + verify(audioManager, never()).registerMuteAwaitConnectionCallback(any(), any()) + } + + @Test + fun startListening_audioManagerCallbackRegistered() { + muteAwaitConnectionManager.startListening() + + verify(audioManager).registerMuteAwaitConnectionCallback(any(), any()) + } + + @Test + fun stopListening_audioManagerCallbackUnregistered() { + muteAwaitConnectionManager.stopListening() + + verify(audioManager).unregisterMuteAwaitConnectionCallback(any()) + } + + @Test + fun startListening_audioManagerHasNoMuteAwaitDevice_localMediaMangerNotNotified() { whenever(audioManager.mutingExpectedDevice).thenReturn(null) - instantiateManager() + muteAwaitConnectionManager.startListening() verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any()) } @Test - fun constructor_audioManagerHasMuteAwaitDevice_localMediaMangerNotified() { + fun startListening_audioManagerHasMuteAwaitDevice_localMediaMangerNotified() { whenever(audioManager.mutingExpectedDevice).thenReturn(DEVICE) - instantiateManager() + muteAwaitConnectionManager.startListening() verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(DEVICE_NAME), eq(icon)) } @Test fun onMutedUntilConnection_notUsageMedia_localMediaManagerNotNotified() { - instantiateManager() + muteAwaitConnectionManager.startListening() + val muteAwaitListener = getMuteAwaitListener() muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_UNKNOWN)) @@ -94,7 +119,9 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { @Test fun onMutedUntilConnection_isUsageMedia_localMediaManagerNotified() { - instantiateManager() + muteAwaitConnectionManager.startListening() + val muteAwaitListener = getMuteAwaitListener() + muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA)) @@ -103,7 +130,9 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { @Test fun onUnmutedEvent_noDeviceMutedBefore_localMediaManagerNotNotified() { - instantiateManager() + muteAwaitConnectionManager.startListening() + val muteAwaitListener = getMuteAwaitListener() + muteAwaitListener.onUnmutedEvent(EVENT_CONNECTION, DEVICE, intArrayOf(USAGE_MEDIA)) verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any()) @@ -111,7 +140,8 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { @Test fun onUnmutedEvent_notSameDevice_localMediaManagerNotNotified() { - instantiateManager() + muteAwaitConnectionManager.startListening() + val muteAwaitListener = getMuteAwaitListener() muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA)) reset(localMediaManager) @@ -130,7 +160,8 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { @Test fun onUnmutedEvent_notUsageMedia_localMediaManagerNotNotified() { - instantiateManager() + muteAwaitConnectionManager.startListening() + val muteAwaitListener = getMuteAwaitListener() muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA)) reset(localMediaManager) @@ -141,7 +172,8 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { @Test fun onUnmutedEvent_sameDeviceAndUsageMedia_localMediaManagerNotified() { - instantiateManager() + muteAwaitConnectionManager.startListening() + val muteAwaitListener = getMuteAwaitListener() muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA)) reset(localMediaManager) @@ -150,20 +182,12 @@ class MediaMuteAwaitConnectionManagerTest : SysuiTestCase() { verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(null), eq(null)) } - // Some classes test the constructor, so don't instantiate the manager in @SetUp. - private fun instantiateManager() { - muteAwaitConnectionManager = MediaMuteAwaitConnectionManager( - FakeExecutor(FakeSystemClock()), - localMediaManager, - context, - deviceIconUtil - ) - + private fun getMuteAwaitListener(): AudioManager.MuteAwaitConnectionCallback { val listenerCaptor = ArgumentCaptor.forClass( AudioManager.MuteAwaitConnectionCallback::class.java ) verify(audioManager).registerMuteAwaitConnectionCallback(any(), listenerCaptor.capture()) - muteAwaitListener = listenerCaptor.value!! + return listenerCaptor.value!! } }