Merge "Disable output switcher based on available routing" into rvc-dev

This commit is contained in:
Robert Snoeberger
2020-05-21 18:24:47 +00:00
committed by Android (Google) Code Review
7 changed files with 159 additions and 50 deletions

View File

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

View File

@@ -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.");

View File

@@ -47,6 +47,7 @@ data class MediaAction(
/** State of the media device. */
data class MediaDeviceData(
val enabled: Boolean,
val icon: Drawable?,
val name: String?
)

View File

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

View File

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

View File

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

View File

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