[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
This commit is contained in:
@@ -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
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<MediaTttCommandLineHelper> getMediaTttCommandLineHelper();
|
||||
|
||||
/** */
|
||||
Optional<MediaMuteAwaitConnectionCli> getMediaMuteAwaitConnectionCli();
|
||||
|
||||
/**
|
||||
* Member injection into the supplied argument.
|
||||
*/
|
||||
|
||||
@@ -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 ==========================
|
||||
|
||||
@@ -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) }
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we support displaying information about mute await connections.
|
||||
*/
|
||||
fun areMuteAwaitConnectionsEnabled() = featureFlags.isEnabled(Flags.MEDIA_MUTE_AWAIT)
|
||||
}
|
||||
|
||||
@@ -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<MediaMuteAwaitConnectionCli> 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
|
||||
|
||||
@@ -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<String>) {
|
||||
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
|
||||
@@ -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)!!
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
Reference in New Issue
Block a user