Merge changes from topic "ccassidy-media-sass"

* changes:
  [Media SASS] Unregister the muteAwaitConnectionCallback when the Entry stops.
  [Media SASS] Choose the correct device icon based on the device attributes.
  [Media SASS] Display about-to-connect device names in the media player device chip.
This commit is contained in:
Caitlin Cassidy
2022-02-10 19:45:58 +00:00
committed by Android (Google) Code Review
14 changed files with 849 additions and 24 deletions

View File

@@ -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<Integer, Device> mAudioDeviceTypeToIconMap = new HashMap<>();
// A map from a @MediaRoute2Info.Type to full device information.
private final Map<Integer, Device> 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<Device> 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;
}
}
}

View File

@@ -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
) {}
}
/**

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -25,6 +25,7 @@ import com.android.systemui.SystemUIAppComponentFactory;
import com.android.systemui.dagger.qualifiers.PerUser;
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;
@@ -150,6 +151,7 @@ public interface SysUIComponent {
getMediaTttChipControllerSender();
getMediaTttChipControllerReceiver();
getMediaTttCommandLineHelper();
getMediaMuteAwaitConnectionCli();
getUnfoldLatencyTracker().init();
getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init);
getFoldStateLogger().ifPresent(FoldStateLogger::init);
@@ -226,6 +228,9 @@ public interface SysUIComponent {
/** */
Optional<MediaTttCommandLineHelper> getMediaTttCommandLineHelper();
/** */
Optional<MediaMuteAwaitConnectionCli> getMediaMuteAwaitConnectionCli();
/**
* Returns {@link CoreStartable}s that should be started with the application.
*/

View File

@@ -142,6 +142,7 @@ public class Flags {
public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, true);
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 ==========================

View File

@@ -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,8 @@ 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
import java.util.concurrent.Executor
@@ -41,6 +44,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 +84,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)
val muteAwaitConnectionManager =
muteAwaitConnectionManagerFactory.create(localMediaManager)
entry = Entry(
key,
oldKey,
controller,
localMediaManager,
muteAwaitConnectionManager
)
entries[key] = entry
entry.start()
}
@@ -126,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
@@ -142,11 +155,15 @@ 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 {
localMediaManager.registerCallback(this)
localMediaManager.startScan()
muteAwaitConnectionManager?.startListening()
playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
controller?.registerCallback(this)
updateCurrent()
@@ -159,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<String>) {
@@ -197,8 +215,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) }

View File

@@ -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)
}

View File

@@ -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

View File

@@ -0,0 +1,75 @@
/*
* 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.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,
/* type= */ Integer.parseInt(args[0]),
ADDRESS,
/* name= */ args[1],
listOf(),
listOf(),
)
val startOrCancel = args[2]
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`; was $startOrCancel")
}
}
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar $MEDIA_MUTE_AWAIT_COMMAND " +
"[type] [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

View File

@@ -0,0 +1,93 @@
/*
* 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.DeviceIconUtil
import com.android.settingslib.media.LocalMediaManager
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 private val mainExecutor: Executor,
private val localMediaManager: LocalMediaManager,
private val context: Context,
private val deviceIconUtil: DeviceIconUtil
) {
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, device.getIcon())
}
}
override fun onUnmutedEvent(
@UnmuteEvent unmuteEvent: Int,
device: AudioDeviceAttributes,
mutedUsages: IntArray
) {
if (currentMutedDevice == device && USAGE_MEDIA in mutedUsages) {
currentMutedDevice = null
localMediaManager.dispatchAboutToConnectDeviceChanged(null, null)
}
}
}
/** Start listening for mute await events. */
fun startListening() {
audioManager.registerMuteAwaitConnectionCallback(
mainExecutor, muteAwaitConnectionChangeListener
)
val currentDevice = audioManager.mutingExpectedDevice
if (currentDevice != null) {
currentMutedDevice = currentDevice
localMediaManager.dispatchAboutToConnectDeviceChanged(
currentDevice.name, currentDevice.getIcon()
)
}
}
/** Stop listening for mute await events. */
fun stopListening() {
audioManager.unregisterMuteAwaitConnectionCallback(muteAwaitConnectionChangeListener)
}
private fun AudioDeviceAttributes.getIcon(): Drawable {
return deviceIconUtil.getIconFromAudioDeviceType(this.type, context)
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.DeviceIconUtil
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
) {
private val deviceIconUtil = DeviceIconUtil()
/** Creates a [MediaMuteAwaitConnectionManager]. */
fun create(localMediaManager: LocalMediaManager): MediaMuteAwaitConnectionManager? {
if (!mediaFlags.areMuteAwaitConnectionsEnabled()) {
return null
}
return MediaMuteAwaitConnectionManager(
mainExecutor, localMediaManager, context, deviceIconUtil
)
}
}

View File

@@ -30,6 +30,8 @@ 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
@@ -44,6 +46,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 +74,8 @@ 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
@Mock private lateinit var muteAwaitManager: MediaMuteAwaitConnectionManager
private lateinit var fakeFgExecutor: FakeExecutor
private lateinit var fakeBgExecutor: FakeExecutor
@Mock private lateinit var dumpster: DumpManager
@@ -88,14 +93,22 @@ 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.
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)
@@ -146,6 +159,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
manager.onMediaDataRemoved(KEY)
fakeBgExecutor.runAllReady()
verify(lmm).unregisterCallback(any())
verify(muteAwaitManager).stopListening()
}
@Test
@@ -169,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()
@@ -240,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)
@@ -267,6 +283,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)

View File

@@ -0,0 +1,202 @@
/*
* 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.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.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
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 deviceIconUtil: DeviceIconUtil
@Mock
private lateinit var localMediaManager: LocalMediaManager
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)
muteAwaitConnectionManager = MediaMuteAwaitConnectionManager(
FakeExecutor(FakeSystemClock()),
localMediaManager,
context,
deviceIconUtil
)
}
@Test
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)
muteAwaitConnectionManager.startListening()
verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any())
}
@Test
fun startListening_audioManagerHasMuteAwaitDevice_localMediaMangerNotified() {
whenever(audioManager.mutingExpectedDevice).thenReturn(DEVICE)
muteAwaitConnectionManager.startListening()
verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(DEVICE_NAME), eq(icon))
}
@Test
fun onMutedUntilConnection_notUsageMedia_localMediaManagerNotNotified() {
muteAwaitConnectionManager.startListening()
val muteAwaitListener = getMuteAwaitListener()
muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_UNKNOWN))
verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any())
}
@Test
fun onMutedUntilConnection_isUsageMedia_localMediaManagerNotified() {
muteAwaitConnectionManager.startListening()
val muteAwaitListener = getMuteAwaitListener()
muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA))
verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(DEVICE_NAME), eq(icon))
}
@Test
fun onUnmutedEvent_noDeviceMutedBefore_localMediaManagerNotNotified() {
muteAwaitConnectionManager.startListening()
val muteAwaitListener = getMuteAwaitListener()
muteAwaitListener.onUnmutedEvent(EVENT_CONNECTION, DEVICE, intArrayOf(USAGE_MEDIA))
verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any())
}
@Test
fun onUnmutedEvent_notSameDevice_localMediaManagerNotNotified() {
muteAwaitConnectionManager.startListening()
val muteAwaitListener = getMuteAwaitListener()
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() {
muteAwaitConnectionManager.startListening()
val muteAwaitListener = getMuteAwaitListener()
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() {
muteAwaitConnectionManager.startListening()
val muteAwaitListener = getMuteAwaitListener()
muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA))
reset(localMediaManager)
muteAwaitListener.onUnmutedEvent(EVENT_CONNECTION, DEVICE, intArrayOf(USAGE_MEDIA))
verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(null), eq(null))
}
private fun getMuteAwaitListener(): AudioManager.MuteAwaitConnectionCallback {
val listenerCaptor = ArgumentCaptor.forClass(
AudioManager.MuteAwaitConnectionCallback::class.java
)
verify(audioManager).registerMuteAwaitConnectionCallback(any(), listenerCaptor.capture())
return listenerCaptor.value!!
}
}
private const val DEVICE_NAME = "DeviceName"
private val DEVICE = AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT,
AudioDeviceInfo.TYPE_USB_HEADSET,
"address",
DEVICE_NAME,
listOf(),
listOf(),
)