Merge "Disable output switcher based on available routing" into rvc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
de793a8f26
@@ -39,6 +39,7 @@ import android.content.res.Resources;
|
||||
import android.hardware.SensorPrivacyManager;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaRouter2Manager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkScoreManager;
|
||||
import android.net.wifi.WifiManager;
|
||||
@@ -210,6 +211,11 @@ public class SystemServicesModule {
|
||||
return LocalBluetoothManager.create(context, bgHandler, UserHandle.ALL);
|
||||
}
|
||||
|
||||
@Provides
|
||||
static MediaRouter2Manager provideMediaRouter2Manager(Context context) {
|
||||
return MediaRouter2Manager.getInstance(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static NetworkScoreManager provideNetworkScoreManager(Context context) {
|
||||
|
||||
@@ -33,7 +33,6 @@ import android.graphics.drawable.GradientDrawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.media.session.MediaController;
|
||||
import android.media.session.MediaController.PlaybackInfo;
|
||||
import android.media.session.MediaSession;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.service.media.MediaBrowserService;
|
||||
@@ -294,14 +293,6 @@ public class MediaControlPanel {
|
||||
mActivityStarter.startActivity(intent, false, true /* dismissShade */,
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
});
|
||||
final boolean isRemotePlayback;
|
||||
PlaybackInfo playbackInfo = mController.getPlaybackInfo();
|
||||
if (playbackInfo != null) {
|
||||
isRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
|
||||
} else {
|
||||
Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
|
||||
isRemotePlayback = false;
|
||||
}
|
||||
|
||||
ImageView iconView = mViewHolder.getSeamlessIcon();
|
||||
TextView deviceName = mViewHolder.getSeamlessText();
|
||||
@@ -312,18 +303,18 @@ public class MediaControlPanel {
|
||||
rect.setStroke(2, deviceName.getCurrentTextColor());
|
||||
rect.setColor(Color.TRANSPARENT);
|
||||
|
||||
if (isRemotePlayback) {
|
||||
final MediaDeviceData device = data.getDevice();
|
||||
if (device != null && !device.getEnabled()) {
|
||||
mViewHolder.getSeamless().setEnabled(false);
|
||||
// TODO(b/156875717): setEnabled should cause the alpha to change.
|
||||
mViewHolder.getSeamless().setAlpha(0.38f);
|
||||
iconView.setImageResource(R.drawable.ic_hardware_speaker);
|
||||
iconView.setVisibility(View.VISIBLE);
|
||||
deviceName.setText(R.string.media_seamless_remote_device);
|
||||
} else if (data.getDevice() != null && data.getDevice().getIcon() != null
|
||||
&& data.getDevice().getName() != null) {
|
||||
} else if (device != null) {
|
||||
mViewHolder.getSeamless().setEnabled(true);
|
||||
mViewHolder.getSeamless().setAlpha(1f);
|
||||
Drawable icon = data.getDevice().getIcon();
|
||||
Drawable icon = device.getIcon();
|
||||
iconView.setVisibility(View.VISIBLE);
|
||||
|
||||
if (icon instanceof AdaptiveIcon) {
|
||||
@@ -333,7 +324,7 @@ public class MediaControlPanel {
|
||||
} else {
|
||||
iconView.setImageDrawable(icon);
|
||||
}
|
||||
deviceName.setText(data.getDevice().getName());
|
||||
deviceName.setText(device.getName());
|
||||
} else {
|
||||
// Reset to default
|
||||
Log.w(TAG, "device is null. Not binding output chip.");
|
||||
|
||||
@@ -47,6 +47,7 @@ data class MediaAction(
|
||||
|
||||
/** State of the media device. */
|
||||
data class MediaDeviceData(
|
||||
val enabled: Boolean,
|
||||
val icon: Drawable?,
|
||||
val name: String?
|
||||
)
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import android.service.notification.StatusBarNotification
|
||||
import android.media.MediaRouter2Manager
|
||||
import android.media.session.MediaSession
|
||||
import android.media.session.MediaController
|
||||
import com.android.settingslib.media.LocalMediaManager
|
||||
import com.android.settingslib.media.MediaDevice
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
@@ -32,6 +36,7 @@ import javax.inject.Singleton
|
||||
class MediaDeviceManager @Inject constructor(
|
||||
private val context: Context,
|
||||
private val localMediaManagerFactory: LocalMediaManagerFactory,
|
||||
private val mr2manager: MediaRouter2Manager,
|
||||
private val featureFlag: MediaFeatureFlag,
|
||||
@Main private val fgExecutor: Executor
|
||||
) {
|
||||
@@ -52,7 +57,10 @@ class MediaDeviceManager @Inject constructor(
|
||||
if (featureFlag.enabled && isMediaNotification(sbn)) {
|
||||
var tok = entries[key]
|
||||
if (tok == null) {
|
||||
tok = Token(key, localMediaManagerFactory.create(sbn.packageName))
|
||||
val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
|
||||
as MediaSession.Token?
|
||||
val controller = MediaController(context, token)
|
||||
tok = Token(key, controller, localMediaManagerFactory.create(sbn.packageName))
|
||||
entries[key] = tok
|
||||
tok.start()
|
||||
}
|
||||
@@ -72,7 +80,8 @@ class MediaDeviceManager @Inject constructor(
|
||||
}
|
||||
|
||||
private fun processDevice(key: String, device: MediaDevice?) {
|
||||
val data = MediaDeviceData(device?.icon, device?.name)
|
||||
val enabled = device != null
|
||||
val data = MediaDeviceData(enabled, device?.icon, device?.name)
|
||||
listeners.forEach {
|
||||
it.onMediaDeviceChanged(key, data)
|
||||
}
|
||||
@@ -87,11 +96,13 @@ class MediaDeviceManager @Inject constructor(
|
||||
|
||||
private inner class Token(
|
||||
val key: String,
|
||||
val controller: MediaController,
|
||||
val localMediaManager: LocalMediaManager
|
||||
) : LocalMediaManager.DeviceCallback {
|
||||
private var started = false
|
||||
private var current: MediaDevice? = null
|
||||
set(value) {
|
||||
if (value != field) {
|
||||
if (!started || value != field) {
|
||||
field = value
|
||||
processDevice(key, value)
|
||||
}
|
||||
@@ -99,19 +110,28 @@ class MediaDeviceManager @Inject constructor(
|
||||
fun start() {
|
||||
localMediaManager.registerCallback(this)
|
||||
localMediaManager.startScan()
|
||||
current = localMediaManager.getCurrentConnectedDevice()
|
||||
updateCurrent()
|
||||
started = true
|
||||
}
|
||||
fun stop() {
|
||||
started = false
|
||||
localMediaManager.stopScan()
|
||||
localMediaManager.unregisterCallback(this)
|
||||
}
|
||||
override fun onDeviceListUpdate(devices: List<MediaDevice>?) = fgExecutor.execute {
|
||||
current = localMediaManager.getCurrentConnectedDevice()
|
||||
updateCurrent()
|
||||
}
|
||||
override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) {
|
||||
fgExecutor.execute {
|
||||
current = device
|
||||
updateCurrent()
|
||||
}
|
||||
}
|
||||
private fun updateCurrent() {
|
||||
val device = localMediaManager.getCurrentConnectedDevice()
|
||||
val route = mr2manager.getRoutingSessionForMediaController(controller)
|
||||
// If we get a null route, then don't trust the device. Just set to null to disable the
|
||||
// output switcher chip.
|
||||
current = if (route != null) device else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
private lateinit var action4: ImageButton
|
||||
|
||||
private lateinit var session: MediaSession
|
||||
private val device = MediaDeviceData(true, null, DEVICE_NAME)
|
||||
private val disabledDevice = MediaDeviceData(false, null, null)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@@ -181,7 +183,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun bindWhenUnattached() {
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, null, null, MediaDeviceData(null, DEVICE_NAME))
|
||||
emptyList(), PACKAGE, null, null, device)
|
||||
player.bind(state)
|
||||
assertThat(player.isPlaying()).isFalse()
|
||||
}
|
||||
@@ -190,8 +192,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
fun bindText() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null,
|
||||
MediaDeviceData(null, DEVICE_NAME))
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device)
|
||||
player.bind(state)
|
||||
assertThat(appName.getText()).isEqualTo(APP)
|
||||
assertThat(titleText.getText()).isEqualTo(TITLE)
|
||||
@@ -202,9 +203,40 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
fun bindBackgroundColor() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null,
|
||||
MediaDeviceData(null, DEVICE_NAME))
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device)
|
||||
player.bind(state)
|
||||
assertThat(background.getBackgroundTintList()).isEqualTo(ColorStateList.valueOf(BG_COLOR))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bindDevice() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device)
|
||||
player.bind(state)
|
||||
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
|
||||
assertThat(seamless.isEnabled()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bindDisabledDevice() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice)
|
||||
player.bind(state)
|
||||
assertThat(seamless.isEnabled()).isFalse()
|
||||
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
|
||||
R.string.media_seamless_remote_device))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bindNullDevice() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, null)
|
||||
player.bind(state)
|
||||
assertThat(seamless.isEnabled()).isTrue()
|
||||
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
|
||||
com.android.internal.R.string.ext_media_seamless_action))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
||||
|
||||
mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null,
|
||||
new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null);
|
||||
mDeviceData = new MediaDeviceData(null, DEVICE_NAME);
|
||||
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,6 +18,8 @@ package com.android.systemui.media
|
||||
|
||||
import android.app.Notification
|
||||
import android.media.MediaMetadata
|
||||
import android.media.MediaRouter2Manager
|
||||
import android.media.RoutingSessionInfo
|
||||
import android.media.session.MediaSession
|
||||
import android.media.session.PlaybackState
|
||||
import android.os.Process
|
||||
@@ -36,16 +38,18 @@ import com.google.common.truth.Truth.assertThat
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
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
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
|
||||
private const val KEY = "TEST_KEY"
|
||||
private const val PACKAGE = "PKG"
|
||||
@@ -62,34 +66,34 @@ private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||
public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||
|
||||
private lateinit var manager: MediaDeviceManager
|
||||
|
||||
@Mock private lateinit var lmmFactory: LocalMediaManagerFactory
|
||||
@Mock private lateinit var lmm: LocalMediaManager
|
||||
@Mock private lateinit var mr2: MediaRouter2Manager
|
||||
@Mock private lateinit var featureFlag: MediaFeatureFlag
|
||||
private lateinit var fakeExecutor: FakeExecutor
|
||||
|
||||
@Mock private lateinit var listener: MediaDeviceManager.Listener
|
||||
@Mock private lateinit var device: MediaDevice
|
||||
@Mock private lateinit var route: RoutingSessionInfo
|
||||
private lateinit var session: MediaSession
|
||||
private lateinit var metadataBuilder: MediaMetadata.Builder
|
||||
private lateinit var playbackBuilder: PlaybackState.Builder
|
||||
private lateinit var notifBuilder: Notification.Builder
|
||||
private lateinit var sbn: StatusBarNotification
|
||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
lmmFactory = mock(LocalMediaManagerFactory::class.java)
|
||||
lmm = mock(LocalMediaManager::class.java)
|
||||
device = mock(MediaDevice::class.java)
|
||||
fun setUp() {
|
||||
fakeExecutor = FakeExecutor(FakeSystemClock())
|
||||
manager = MediaDeviceManager(context, lmmFactory, mr2, featureFlag, fakeExecutor)
|
||||
manager.addListener(listener)
|
||||
|
||||
// Configure mocks.
|
||||
whenever(device.name).thenReturn(DEVICE_NAME)
|
||||
whenever(lmmFactory.create(PACKAGE)).thenReturn(lmm)
|
||||
whenever(lmm.getCurrentConnectedDevice()).thenReturn(device)
|
||||
featureFlag = mock(MediaFeatureFlag::class.java)
|
||||
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(route)
|
||||
whenever(featureFlag.enabled).thenReturn(true)
|
||||
|
||||
fakeExecutor = FakeExecutor(FakeSystemClock())
|
||||
|
||||
manager = MediaDeviceManager(context, lmmFactory, featureFlag, fakeExecutor)
|
||||
|
||||
// Create a media sesssion and notification for testing.
|
||||
metadataBuilder = MediaMetadata.Builder().apply {
|
||||
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
|
||||
@@ -144,52 +148,107 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||
verify(lmm).unregisterCallback(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deviceEventOnAddNotification() {
|
||||
// WHEN a notification is added
|
||||
manager.onNotificationAdded(KEY, sbn)
|
||||
val deviceCallback = captureCallback()
|
||||
// THEN the update is dispatched to the listener
|
||||
val data = captureDeviceData(KEY)
|
||||
assertThat(data.enabled).isTrue()
|
||||
assertThat(data.name).isEqualTo(DEVICE_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deviceListUpdate() {
|
||||
val listener = mock(MediaDeviceManager.Listener::class.java)
|
||||
manager.addListener(listener)
|
||||
manager.onNotificationAdded(KEY, sbn)
|
||||
val deviceCallback = captureCallback()
|
||||
// WHEN the device list changes
|
||||
deviceCallback.onDeviceListUpdate(mutableListOf(device))
|
||||
assertThat(fakeExecutor.runAllReady()).isEqualTo(1)
|
||||
// THEN the update is dispatched to the listener
|
||||
val captor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
|
||||
verify(listener).onMediaDeviceChanged(eq(KEY), captor.capture())
|
||||
val data = captor.getValue()
|
||||
val data = captureDeviceData(KEY)
|
||||
assertThat(data.enabled).isTrue()
|
||||
assertThat(data.name).isEqualTo(DEVICE_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectedDeviceStateChanged() {
|
||||
val listener = mock(MediaDeviceManager.Listener::class.java)
|
||||
manager.addListener(listener)
|
||||
manager.onNotificationAdded(KEY, sbn)
|
||||
val deviceCallback = captureCallback()
|
||||
// WHEN the selected device changes state
|
||||
deviceCallback.onSelectedDeviceStateChanged(device, 1)
|
||||
assertThat(fakeExecutor.runAllReady()).isEqualTo(1)
|
||||
// THEN the update is dispatched to the listener
|
||||
val captor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
|
||||
verify(listener).onMediaDeviceChanged(eq(KEY), captor.capture())
|
||||
val data = captor.getValue()
|
||||
val data = captureDeviceData(KEY)
|
||||
assertThat(data.enabled).isTrue()
|
||||
assertThat(data.name).isEqualTo(DEVICE_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun listenerReceivesKeyRemoved() {
|
||||
manager.onNotificationAdded(KEY, sbn)
|
||||
val listener = mock(MediaDeviceManager.Listener::class.java)
|
||||
manager.addListener(listener)
|
||||
// WHEN the notification is removed
|
||||
manager.onNotificationRemoved(KEY)
|
||||
// THEN the listener receives key removed event
|
||||
verify(listener).onKeyRemoved(eq(KEY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deviceDisabledWhenMR2ReturnsNullRouteInfo() {
|
||||
// GIVEN that MR2Manager returns null for routing session
|
||||
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
|
||||
// WHEN a notification is added
|
||||
manager.onNotificationAdded(KEY, sbn)
|
||||
// THEN the device is disabled
|
||||
val data = captureDeviceData(KEY)
|
||||
assertThat(data.enabled).isFalse()
|
||||
assertThat(data.name).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceChanged() {
|
||||
// GIVEN a notif is added
|
||||
manager.onNotificationAdded(KEY, sbn)
|
||||
reset(listener)
|
||||
// AND MR2Manager returns null for routing session
|
||||
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
|
||||
// WHEN the selected device changes state
|
||||
val deviceCallback = captureCallback()
|
||||
deviceCallback.onSelectedDeviceStateChanged(device, 1)
|
||||
fakeExecutor.runAllReady()
|
||||
// THEN the device is disabled
|
||||
val data = captureDeviceData(KEY)
|
||||
assertThat(data.enabled).isFalse()
|
||||
assertThat(data.name).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceListUpdate() {
|
||||
// GIVEN a notif is added
|
||||
manager.onNotificationAdded(KEY, sbn)
|
||||
reset(listener)
|
||||
// GIVEN that MR2Manager returns null for routing session
|
||||
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
|
||||
// WHEN the selected device changes state
|
||||
val deviceCallback = captureCallback()
|
||||
deviceCallback.onDeviceListUpdate(mutableListOf(device))
|
||||
fakeExecutor.runAllReady()
|
||||
// THEN the device is disabled
|
||||
val data = captureDeviceData(KEY)
|
||||
assertThat(data.enabled).isFalse()
|
||||
assertThat(data.name).isNull()
|
||||
}
|
||||
|
||||
fun captureCallback(): LocalMediaManager.DeviceCallback {
|
||||
val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java)
|
||||
verify(lmm).registerCallback(captor.capture())
|
||||
return captor.getValue()
|
||||
}
|
||||
|
||||
fun captureDeviceData(key: String): MediaDeviceData {
|
||||
val captor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
|
||||
verify(listener).onMediaDeviceChanged(eq(key), captor.capture())
|
||||
return captor.getValue()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user