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(), +)