Merge "[Le Audio] Add a new BroadcastDialog in SystemUI from MediaPanel" into tm-qpr-dev
This commit is contained in:
@@ -70,6 +70,7 @@ import com.android.systemui.R;
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||
import com.android.systemui.animation.GhostedViewLaunchAnimatorController;
|
||||
import com.android.systemui.animation.Interpolators;
|
||||
import com.android.systemui.bluetooth.BroadcastDialogController;
|
||||
import com.android.systemui.broadcast.BroadcastSender;
|
||||
import com.android.systemui.dagger.qualifiers.Background;
|
||||
import com.android.systemui.dagger.qualifiers.Main;
|
||||
@@ -188,6 +189,11 @@ public class MediaControlPanel {
|
||||
private final SeekBarViewModel.EnabledChangeListener mEnabledChangeListener =
|
||||
this::setIsSeekBarEnabled;
|
||||
|
||||
private final BroadcastDialogController mBroadcastDialogController;
|
||||
private boolean mIsCurrentBroadcastedApp = false;
|
||||
private boolean mShowBroadcastDialogButton = false;
|
||||
private String mSwitchBroadcastApp;
|
||||
|
||||
/**
|
||||
* Initialize a new control panel
|
||||
*
|
||||
@@ -213,7 +219,8 @@ public class MediaControlPanel {
|
||||
MediaUiEventLogger logger,
|
||||
KeyguardStateController keyguardStateController,
|
||||
ActivityIntentHelper activityIntentHelper,
|
||||
NotificationLockscreenUserManager lockscreenUserManager) {
|
||||
NotificationLockscreenUserManager lockscreenUserManager,
|
||||
BroadcastDialogController broadcastDialogController) {
|
||||
mContext = context;
|
||||
mBackgroundExecutor = backgroundExecutor;
|
||||
mMainExecutor = mainExecutor;
|
||||
@@ -230,6 +237,7 @@ public class MediaControlPanel {
|
||||
mKeyguardStateController = keyguardStateController;
|
||||
mActivityIntentHelper = activityIntentHelper;
|
||||
mLockscreenUserManager = lockscreenUserManager;
|
||||
mBroadcastDialogController = broadcastDialogController;
|
||||
|
||||
mSeekBarViewModel.setLogSeek(() -> {
|
||||
if (mPackageName != null && mInstanceId != null) {
|
||||
@@ -449,7 +457,10 @@ public class MediaControlPanel {
|
||||
final MediaController controller = getController();
|
||||
mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
|
||||
|
||||
bindOutputSwitcherChip(data);
|
||||
// Show the broadcast dialog button only when the le audio is enabled.
|
||||
mShowBroadcastDialogButton =
|
||||
data.getDevice() != null && data.getDevice().getShowBroadcastButton();
|
||||
bindOutputSwitcherAndBroadcastButton(mShowBroadcastDialogButton, data);
|
||||
bindGutsMenuForPlayer(data);
|
||||
bindPlayerContentDescription(data);
|
||||
bindScrubbingTime(data);
|
||||
@@ -467,21 +478,40 @@ public class MediaControlPanel {
|
||||
Trace.endSection();
|
||||
}
|
||||
|
||||
private void bindOutputSwitcherChip(MediaData data) {
|
||||
// Output switcher chip
|
||||
private void bindOutputSwitcherAndBroadcastButton(boolean showBroadcastButton, MediaData data) {
|
||||
ViewGroup seamlessView = mMediaViewHolder.getSeamless();
|
||||
seamlessView.setVisibility(View.VISIBLE);
|
||||
ImageView iconView = mMediaViewHolder.getSeamlessIcon();
|
||||
TextView deviceName = mMediaViewHolder.getSeamlessText();
|
||||
final MediaDeviceData device = data.getDevice();
|
||||
|
||||
// Disable clicking on output switcher for invalid devices and resumption controls
|
||||
final boolean seamlessDisabled = (device != null && !device.getEnabled())
|
||||
|| data.getResumption();
|
||||
final float seamlessAlpha = seamlessDisabled ? DISABLED_ALPHA : 1.0f;
|
||||
mMediaViewHolder.getSeamlessButton().setAlpha(seamlessAlpha);
|
||||
seamlessView.setEnabled(!seamlessDisabled);
|
||||
CharSequence deviceString = mContext.getString(R.string.media_seamless_other_device);
|
||||
final boolean enabled;
|
||||
final boolean seamlessDisabled;
|
||||
final int iconResource;
|
||||
CharSequence deviceString;
|
||||
if (showBroadcastButton) {
|
||||
// TODO(b/233698402): Use the package name instead of app label to avoid the
|
||||
// unexpected result.
|
||||
mIsCurrentBroadcastedApp = device != null
|
||||
&& TextUtils.equals(device.getName(),
|
||||
MediaDataUtils.getAppLabel(mContext, mPackageName, mContext.getString(
|
||||
R.string.bt_le_audio_broadcast_dialog_unknown_name)));
|
||||
seamlessDisabled = !mIsCurrentBroadcastedApp;
|
||||
// Always be enabled if the broadcast button is shown
|
||||
enabled = true;
|
||||
deviceString = mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name);
|
||||
iconResource = R.drawable.settings_input_antenna;
|
||||
} else {
|
||||
// Disable clicking on output switcher for invalid devices and resumption controls
|
||||
seamlessDisabled = (device != null && !device.getEnabled()) || data.getResumption();
|
||||
enabled = !seamlessDisabled;
|
||||
deviceString = mContext.getString(R.string.media_seamless_other_device);
|
||||
iconResource = R.drawable.ic_media_home_devices;
|
||||
}
|
||||
|
||||
mMediaViewHolder.getSeamlessButton().setAlpha(seamlessDisabled ? DISABLED_ALPHA : 1.0f);
|
||||
seamlessView.setEnabled(enabled);
|
||||
|
||||
if (device != null) {
|
||||
Drawable icon = device.getIcon();
|
||||
if (icon instanceof AdaptiveIcon) {
|
||||
@@ -494,7 +524,7 @@ public class MediaControlPanel {
|
||||
deviceString = device.getName();
|
||||
} else {
|
||||
// Set to default icon
|
||||
iconView.setImageResource(R.drawable.ic_media_home_devices);
|
||||
iconView.setImageResource(iconResource);
|
||||
}
|
||||
deviceName.setText(deviceString);
|
||||
seamlessView.setContentDescription(deviceString);
|
||||
@@ -503,21 +533,39 @@ public class MediaControlPanel {
|
||||
if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
return;
|
||||
}
|
||||
mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
|
||||
if (device.getIntent() != null) {
|
||||
if (device.getIntent().isActivity()) {
|
||||
mActivityStarter.startActivity(
|
||||
device.getIntent().getIntent(), true);
|
||||
|
||||
if (showBroadcastButton) {
|
||||
// If the current media app is not broadcasted and users press the outputer
|
||||
// button, we should pop up the broadcast dialog to check do they want to
|
||||
// switch broadcast to the other media app, otherwise we still pop up the
|
||||
// media output dialog.
|
||||
if (!mIsCurrentBroadcastedApp) {
|
||||
mLogger.logOpenBroadcastDialog(mUid, mPackageName, mInstanceId);
|
||||
mSwitchBroadcastApp = device.getName().toString();
|
||||
mBroadcastDialogController.createBroadcastDialog(mSwitchBroadcastApp,
|
||||
mPackageName, true, mMediaViewHolder.getSeamlessButton());
|
||||
} else {
|
||||
try {
|
||||
device.getIntent().send();
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.e(TAG, "Device pending intent was canceled");
|
||||
}
|
||||
mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
|
||||
mMediaOutputDialogFactory.create(mPackageName, true,
|
||||
mMediaViewHolder.getSeamlessButton());
|
||||
}
|
||||
} else {
|
||||
mMediaOutputDialogFactory.create(mPackageName, true,
|
||||
mMediaViewHolder.getSeamlessButton());
|
||||
mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
|
||||
if (device.getIntent() != null) {
|
||||
if (device.getIntent().isActivity()) {
|
||||
mActivityStarter.startActivity(
|
||||
device.getIntent().getIntent(), true);
|
||||
} else {
|
||||
try {
|
||||
device.getIntent().send();
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.e(TAG, "Device pending intent was canceled");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mMediaOutputDialogFactory.create(mPackageName, true,
|
||||
mMediaViewHolder.getSeamlessButton());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,5 +215,8 @@ data class MediaDeviceData
|
||||
val intent: PendingIntent? = null,
|
||||
|
||||
/** Unique id for this device */
|
||||
val id: String? = null
|
||||
val id: String? = null,
|
||||
|
||||
/** Whether or not to show the broadcast button */
|
||||
val showBroadcastButton: Boolean
|
||||
)
|
||||
|
||||
@@ -686,7 +686,8 @@ class MediaDataManager(
|
||||
val enabled = deviceIntent != null && deviceIntent.isActivity
|
||||
val deviceDrawable = Icon.createWithResource(sbn.packageName, deviceIcon)
|
||||
.loadDrawable(sbn.getPackageContext(context))
|
||||
device = MediaDeviceData(enabled, deviceDrawable, deviceName, deviceIntent)
|
||||
device = MediaDeviceData(enabled, deviceDrawable, deviceName, deviceIntent,
|
||||
showBroadcastButton = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,15 +16,23 @@
|
||||
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.bluetooth.BluetoothLeBroadcast
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.MediaRouter2Manager
|
||||
import android.media.session.MediaController
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothManager
|
||||
import com.android.settingslib.media.LocalMediaManager
|
||||
import com.android.settingslib.media.MediaDevice
|
||||
import com.android.systemui.Dumpable
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.dump.DumpManager
|
||||
@@ -36,16 +44,20 @@ import java.util.concurrent.Executor
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val PLAYBACK_TYPE_UNKNOWN = 0
|
||||
private const val TAG = "MediaDeviceManager"
|
||||
private const val DEBUG = true
|
||||
|
||||
/**
|
||||
* Provides information about the route (ie. device) where playback is occurring.
|
||||
*/
|
||||
class MediaDeviceManager @Inject constructor(
|
||||
private val context: Context,
|
||||
private val controllerFactory: MediaControllerFactory,
|
||||
private val localMediaManagerFactory: LocalMediaManagerFactory,
|
||||
private val mr2manager: MediaRouter2Manager,
|
||||
private val muteAwaitConnectionManagerFactory: MediaMuteAwaitConnectionManagerFactory,
|
||||
private val configurationController: ConfigurationController,
|
||||
private val localBluetoothManager: LocalBluetoothManager?,
|
||||
@Main private val fgExecutor: Executor,
|
||||
@Background private val bgExecutor: Executor,
|
||||
dumpManager: DumpManager
|
||||
@@ -147,7 +159,8 @@ class MediaDeviceManager @Inject constructor(
|
||||
val controller: MediaController?,
|
||||
val localMediaManager: LocalMediaManager,
|
||||
val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager?
|
||||
) : LocalMediaManager.DeviceCallback, MediaController.Callback() {
|
||||
) : LocalMediaManager.DeviceCallback, MediaController.Callback(),
|
||||
BluetoothLeBroadcast.Callback {
|
||||
|
||||
val token
|
||||
get() = controller?.sessionToken
|
||||
@@ -166,7 +179,7 @@ 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: AboutToConnectDevice? = null
|
||||
|
||||
private var broadcastDescription: String? = null
|
||||
private val configListener = object : ConfigurationController.ConfigurationListener {
|
||||
override fun onLocaleListChanged() {
|
||||
updateCurrent()
|
||||
@@ -238,7 +251,11 @@ class MediaDeviceManager @Inject constructor(
|
||||
) {
|
||||
aboutToConnectDeviceOverride = AboutToConnectDevice(
|
||||
fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress),
|
||||
backupMediaDeviceData = MediaDeviceData(enabled = true, deviceIcon, deviceName)
|
||||
backupMediaDeviceData = MediaDeviceData(
|
||||
/* enabled */ enabled = true,
|
||||
/* icon */ deviceIcon,
|
||||
/* name */ deviceName,
|
||||
/* showBroadcastButton */ showBroadcastButton = false)
|
||||
)
|
||||
updateCurrent()
|
||||
}
|
||||
@@ -248,23 +265,127 @@ class MediaDeviceManager @Inject constructor(
|
||||
updateCurrent()
|
||||
}
|
||||
|
||||
|
||||
override fun onBroadcastStarted(reason: Int, broadcastId: Int) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBroadcastStarted(), reason = $reason , broadcastId = $broadcastId")
|
||||
}
|
||||
updateCurrent()
|
||||
}
|
||||
|
||||
override fun onBroadcastStartFailed(reason: Int) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBroadcastStartFailed(), reason = $reason")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBroadcastMetadataChanged(broadcastId: Int,
|
||||
metadata: BluetoothLeBroadcastMetadata) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " +
|
||||
"metadata = $metadata")
|
||||
}
|
||||
updateCurrent()
|
||||
}
|
||||
|
||||
override fun onBroadcastStopped(reason: Int, broadcastId: Int) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBroadcastStopped(), reason = $reason , broadcastId = $broadcastId")
|
||||
|
||||
}
|
||||
updateCurrent()
|
||||
}
|
||||
|
||||
override fun onBroadcastStopFailed(reason: Int) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBroadcastStopFailed(), reason = $reason")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBroadcastUpdated(), reason = $reason , broadcastId = $broadcastId")
|
||||
}
|
||||
updateCurrent()
|
||||
}
|
||||
|
||||
override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBroadcastUpdateFailed(), reason = $reason , " +
|
||||
"broadcastId = $broadcastId")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlaybackStarted(reason: Int, broadcastId: Int) {}
|
||||
|
||||
override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
|
||||
|
||||
@WorkerThread
|
||||
private fun updateCurrent() {
|
||||
val aboutToConnect = aboutToConnectDeviceOverride
|
||||
if (aboutToConnect != null &&
|
||||
aboutToConnect.fullMediaDevice == null &&
|
||||
aboutToConnect.backupMediaDeviceData != null) {
|
||||
if (isLeAudioBroadcastEnabled()) {
|
||||
current = MediaDeviceData(
|
||||
/* enabled */ true,
|
||||
/* icon */ context.getDrawable(R.drawable.settings_input_antenna),
|
||||
/* name */ broadcastDescription,
|
||||
/* intent */ null,
|
||||
/* showBroadcastButton */ showBroadcastButton = true)
|
||||
} else {
|
||||
val aboutToConnect = aboutToConnectDeviceOverride
|
||||
if (aboutToConnect != null &&
|
||||
aboutToConnect.fullMediaDevice == null &&
|
||||
aboutToConnect.backupMediaDeviceData != null) {
|
||||
// Only use [backupMediaDeviceData] when we don't have [fullMediaDevice].
|
||||
current = aboutToConnect.backupMediaDeviceData
|
||||
return
|
||||
}
|
||||
val device = aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice
|
||||
val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
|
||||
}
|
||||
val device = aboutToConnect?.fullMediaDevice
|
||||
?: localMediaManager.currentConnectedDevice
|
||||
val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
|
||||
|
||||
// If we have a controller but get a null route, then don't trust the device
|
||||
val enabled = device != null && (controller == null || route != null)
|
||||
val name = route?.name?.toString() ?: device?.name
|
||||
current = MediaDeviceData(enabled, device?.iconWithoutBackground, name, id = device?.id)
|
||||
// If we have a controller but get a null route, then don't trust the device
|
||||
val enabled = device != null && (controller == null || route != null)
|
||||
val name = route?.name?.toString() ?: device?.name
|
||||
current = MediaDeviceData(enabled, device?.iconWithoutBackground, name,
|
||||
id = device?.id, showBroadcastButton = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLeAudioBroadcastEnabled(): Boolean {
|
||||
if (localBluetoothManager != null) {
|
||||
val profileManager = localBluetoothManager.profileManager
|
||||
if (profileManager != null) {
|
||||
val bluetoothLeBroadcast = profileManager.leAudioBroadcastProfile
|
||||
if (bluetoothLeBroadcast != null && bluetoothLeBroadcast.isEnabled(null)) {
|
||||
getBroadcastingInfo(bluetoothLeBroadcast)
|
||||
return true
|
||||
} else if (DEBUG) {
|
||||
Log.d(TAG, "Can not get LocalBluetoothLeBroadcast")
|
||||
}
|
||||
} else if (DEBUG) {
|
||||
Log.d(TAG, "Can not get LocalBluetoothProfileManager")
|
||||
}
|
||||
} else if (DEBUG) {
|
||||
Log.d(TAG, "Can not get LocalBluetoothManager")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getBroadcastingInfo(bluetoothLeBroadcast: LocalBluetoothLeBroadcast) {
|
||||
var currentBroadcastedApp = bluetoothLeBroadcast.appSourceName
|
||||
// TODO(b/233698402): Use the package name instead of app label to avoid the
|
||||
// unexpected result.
|
||||
// Check the current media app's name is the same with current broadcast app's name
|
||||
// or not.
|
||||
var mediaApp = MediaDataUtils.getAppLabel(
|
||||
context, localMediaManager.packageName,
|
||||
context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name))
|
||||
var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
|
||||
if (isCurrentBroadcastedApp) {
|
||||
broadcastDescription = context.getString(
|
||||
R.string.broadcasting_description_is_broadcasting)
|
||||
} else {
|
||||
broadcastDescription = currentBroadcastedApp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,11 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger)
|
||||
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP, 0, packageName,
|
||||
instanceId)
|
||||
}
|
||||
|
||||
fun logOpenBroadcastDialog(uid: Int, packageName: String, instanceId: InstanceId) {
|
||||
logger.logWithInstanceId(MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG, uid, packageName,
|
||||
instanceId)
|
||||
}
|
||||
}
|
||||
|
||||
enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
|
||||
@@ -273,7 +278,10 @@ enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
|
||||
MEDIA_RECOMMENDATION_ITEM_TAP(1044),
|
||||
|
||||
@UiEvent(doc = "User tapped on a media recommendation card")
|
||||
MEDIA_RECOMMENDATION_CARD_TAP(1045);
|
||||
MEDIA_RECOMMENDATION_CARD_TAP(1045),
|
||||
|
||||
@UiEvent(doc = "User opened the broadcast dialog from a media control")
|
||||
MEDIA_OPEN_BROADCAST_DIALOG(1079);
|
||||
|
||||
override fun getId() = metricId
|
||||
}
|
||||
@@ -58,6 +58,7 @@ import com.android.internal.logging.InstanceId
|
||||
import com.android.systemui.ActivityIntentHelper
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.bluetooth.BroadcastDialogController
|
||||
import com.android.systemui.broadcast.BroadcastSender
|
||||
import com.android.systemui.media.MediaControlPanel.KEY_SMARTSPACE_APP_NAME
|
||||
import com.android.systemui.media.dialog.MediaOutputDialogFactory
|
||||
@@ -104,6 +105,7 @@ private const val SESSION_ARTIST = "SESSION_ARTIST"
|
||||
private const val SESSION_TITLE = "SESSION_TITLE"
|
||||
private const val DISABLED_DEVICE_NAME = "DISABLED_DEVICE_NAME"
|
||||
private const val REC_APP_NAME = "REC APP NAME"
|
||||
private const val APP_NAME = "APP_NAME"
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@@ -130,6 +132,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Mock private lateinit var mediaCarouselController: MediaCarouselController
|
||||
@Mock private lateinit var falsingManager: FalsingManager
|
||||
@Mock private lateinit var transitionParent: ViewGroup
|
||||
@Mock private lateinit var broadcastDialogController: BroadcastDialogController
|
||||
private lateinit var appIcon: ImageView
|
||||
@Mock private lateinit var albumView: ImageView
|
||||
private lateinit var titleText: TextView
|
||||
@@ -160,8 +163,9 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
private lateinit var dismissText: TextView
|
||||
|
||||
private lateinit var session: MediaSession
|
||||
private val device = MediaDeviceData(true, null, DEVICE_NAME)
|
||||
private val disabledDevice = MediaDeviceData(false, null, DISABLED_DEVICE_NAME)
|
||||
private lateinit var device: MediaDeviceData
|
||||
private val disabledDevice = MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null,
|
||||
showBroadcastButton = false)
|
||||
private lateinit var mediaData: MediaData
|
||||
private val clock = FakeSystemClock()
|
||||
@Mock private lateinit var logger: MediaUiEventLogger
|
||||
@@ -187,6 +191,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
private lateinit var recSubtitle1: TextView
|
||||
private lateinit var recSubtitle2: TextView
|
||||
private lateinit var recSubtitle3: TextView
|
||||
private var shouldShowBroadcastButton: Boolean = false
|
||||
|
||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||
|
||||
@@ -223,7 +228,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
logger,
|
||||
keyguardStateController,
|
||||
activityIntentHelper,
|
||||
lockscreenUserManager) {
|
||||
lockscreenUserManager,
|
||||
broadcastDialogController) {
|
||||
override fun loadAnimator(
|
||||
animId: Int,
|
||||
otionInterpolator: Interpolator,
|
||||
@@ -236,28 +242,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
initGutsViewHolderMocks()
|
||||
initMediaViewHolderMocks()
|
||||
|
||||
// Create media session
|
||||
val metadataBuilder = MediaMetadata.Builder().apply {
|
||||
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
|
||||
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
|
||||
}
|
||||
val playbackBuilder = PlaybackState.Builder().apply {
|
||||
setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
|
||||
setActions(PlaybackState.ACTION_PLAY)
|
||||
}
|
||||
session = MediaSession(context, SESSION_KEY).apply {
|
||||
setMetadata(metadataBuilder.build())
|
||||
setPlaybackState(playbackBuilder.build())
|
||||
}
|
||||
session.setActive(true)
|
||||
|
||||
mediaData = MediaTestUtils.emptyMediaData.copy(
|
||||
artist = ARTIST,
|
||||
song = TITLE,
|
||||
packageName = PACKAGE,
|
||||
token = session.sessionToken,
|
||||
device = device,
|
||||
instanceId = instanceId)
|
||||
initDeviceMediaData(false, DEVICE_NAME)
|
||||
|
||||
// Set up recommendation view
|
||||
initRecommendationViewHolderMocks()
|
||||
@@ -293,6 +278,34 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
whenever(gutsViewHolder.dismissText).thenReturn(dismissText)
|
||||
}
|
||||
|
||||
private fun initDeviceMediaData(shouldShowBroadcastButton: Boolean, name: String) {
|
||||
device = MediaDeviceData(true, null, name, null,
|
||||
showBroadcastButton = shouldShowBroadcastButton)
|
||||
|
||||
// Create media session
|
||||
val metadataBuilder = MediaMetadata.Builder().apply {
|
||||
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
|
||||
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
|
||||
}
|
||||
val playbackBuilder = PlaybackState.Builder().apply {
|
||||
setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
|
||||
setActions(PlaybackState.ACTION_PLAY)
|
||||
}
|
||||
session = MediaSession(context, SESSION_KEY).apply {
|
||||
setMetadata(metadataBuilder.build())
|
||||
setPlaybackState(playbackBuilder.build())
|
||||
}
|
||||
session.setActive(true)
|
||||
|
||||
mediaData = MediaTestUtils.emptyMediaData.copy(
|
||||
artist = ARTIST,
|
||||
song = TITLE,
|
||||
packageName = PACKAGE,
|
||||
token = session.sessionToken,
|
||||
device = device,
|
||||
instanceId = instanceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize elements in media view holder
|
||||
*/
|
||||
@@ -1032,6 +1045,28 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
assertThat(seamless.isEnabled()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bindBroadcastButton() {
|
||||
initMediaViewHolderMocks()
|
||||
initDeviceMediaData(true, APP_NAME)
|
||||
|
||||
val mockAvd0 = mock(AnimatedVectorDrawable::class.java)
|
||||
whenever(mockAvd0.mutate()).thenReturn(mockAvd0)
|
||||
val semanticActions0 = MediaButton(
|
||||
playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)
|
||||
)
|
||||
val state = mediaData.copy(resumption = true, semanticActions = semanticActions0,
|
||||
isPlaying = false)
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(state, PACKAGE)
|
||||
assertThat(seamlessText.getText()).isEqualTo(APP_NAME)
|
||||
assertThat(seamless.isEnabled()).isTrue()
|
||||
|
||||
seamless.callOnClick()
|
||||
|
||||
verify(logger).logOpenBroadcastDialog(anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
|
||||
/* ***** Guts tests for the player ***** */
|
||||
|
||||
@Test
|
||||
|
||||
@@ -79,7 +79,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
||||
new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
|
||||
MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L,
|
||||
InstanceId.fakeInstanceId(-1), -1);
|
||||
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
|
||||
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.bluetooth.BluetoothLeBroadcast
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.MediaRouter2Manager
|
||||
import android.media.RoutingSessionInfo
|
||||
@@ -25,8 +29,12 @@ import android.media.session.MediaSession
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothManager
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
|
||||
import com.android.settingslib.media.LocalMediaManager
|
||||
import com.android.settingslib.media.MediaDevice
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.dump.DumpManager
|
||||
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
|
||||
@@ -42,6 +50,7 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.mock
|
||||
@@ -57,6 +66,8 @@ private const val PACKAGE = "PKG"
|
||||
private const val SESSION_KEY = "SESSION_KEY"
|
||||
private const val DEVICE_NAME = "DEVICE_NAME"
|
||||
private const val REMOTE_DEVICE_NAME = "REMOTE_DEVICE_NAME"
|
||||
private const val BROADCAST_APP_NAME = "BROADCAST_APP_NAME"
|
||||
private const val NORMAL_APP_NAME = "NORMAL_APP_NAME"
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@@ -80,6 +91,12 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||
@Mock private lateinit var controller: MediaController
|
||||
@Mock private lateinit var playbackInfo: PlaybackInfo
|
||||
@Mock private lateinit var configurationController: ConfigurationController
|
||||
@Mock private lateinit var bluetoothLeBroadcast: BluetoothLeBroadcast
|
||||
@Mock private lateinit var localBluetoothProfileManager: LocalBluetoothProfileManager
|
||||
@Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast
|
||||
@Mock private lateinit var packageManager: PackageManager
|
||||
@Mock private lateinit var applicationInfo: ApplicationInfo
|
||||
private lateinit var localBluetoothManager: LocalBluetoothManager
|
||||
private lateinit var session: MediaSession
|
||||
private lateinit var mediaData: MediaData
|
||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||
@@ -88,12 +105,15 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||
fun setUp() {
|
||||
fakeFgExecutor = FakeExecutor(FakeSystemClock())
|
||||
fakeBgExecutor = FakeExecutor(FakeSystemClock())
|
||||
localBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager::class.java)
|
||||
manager = MediaDeviceManager(
|
||||
context,
|
||||
controllerFactory,
|
||||
lmmFactory,
|
||||
mr2,
|
||||
muteAwaitFactory,
|
||||
configurationController,
|
||||
localBluetoothManager,
|
||||
fakeFgExecutor,
|
||||
fakeBgExecutor,
|
||||
dumpster
|
||||
@@ -116,6 +136,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||
token = session.sessionToken)
|
||||
whenever(controllerFactory.create(session.sessionToken))
|
||||
.thenReturn(controller)
|
||||
setupLeAudioConfiguration(false)
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -459,7 +480,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun testRemotePlaybackDeviceOverride() {
|
||||
whenever(route.name).thenReturn(DEVICE_NAME)
|
||||
val deviceData = MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null)
|
||||
val deviceData = MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null,
|
||||
showBroadcastButton = false)
|
||||
val mediaDataWithDevice = mediaData.copy(device = deviceData)
|
||||
|
||||
// GIVEN media data that already has a device set
|
||||
@@ -474,12 +496,95 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||
verify(lmm, never()).registerCallback(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onBroadcastStarted_currentMediaDeviceDataIsBroadcasting() {
|
||||
val broadcastCallback = setupBroadcastCallback()
|
||||
setupLeAudioConfiguration(true)
|
||||
setupBroadcastPackage(BROADCAST_APP_NAME)
|
||||
broadcastCallback.onBroadcastStarted(1, 1)
|
||||
|
||||
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||
fakeBgExecutor.runAllReady()
|
||||
fakeFgExecutor.runAllReady()
|
||||
|
||||
val data = captureDeviceData(KEY)
|
||||
assertThat(data.showBroadcastButton).isTrue()
|
||||
assertThat(data.enabled).isTrue()
|
||||
assertThat(data.name).isEqualTo(context.getString(
|
||||
R.string.broadcasting_description_is_broadcasting))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onBroadcastStarted_currentMediaDeviceDataIsNotBroadcasting() {
|
||||
val broadcastCallback = setupBroadcastCallback()
|
||||
setupLeAudioConfiguration(true)
|
||||
setupBroadcastPackage(NORMAL_APP_NAME)
|
||||
broadcastCallback.onBroadcastStarted(1, 1)
|
||||
|
||||
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||
fakeBgExecutor.runAllReady()
|
||||
fakeFgExecutor.runAllReady()
|
||||
|
||||
val data = captureDeviceData(KEY)
|
||||
assertThat(data.showBroadcastButton).isTrue()
|
||||
assertThat(data.enabled).isTrue()
|
||||
assertThat(data.name).isEqualTo(BROADCAST_APP_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onBroadcastStopped_bluetoothLeBroadcastIsDisabledAndBroadcastingButtonIsGone() {
|
||||
val broadcastCallback = setupBroadcastCallback()
|
||||
setupLeAudioConfiguration(false)
|
||||
broadcastCallback.onBroadcastStopped(1, 1)
|
||||
|
||||
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||
fakeBgExecutor.runAllReady()
|
||||
fakeFgExecutor.runAllReady()
|
||||
|
||||
val data = captureDeviceData(KEY)
|
||||
assertThat(data.showBroadcastButton).isFalse()
|
||||
}
|
||||
|
||||
fun captureCallback(): LocalMediaManager.DeviceCallback {
|
||||
val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java)
|
||||
verify(lmm).registerCallback(captor.capture())
|
||||
return captor.getValue()
|
||||
}
|
||||
|
||||
fun setupBroadcastCallback(): BluetoothLeBroadcast.Callback {
|
||||
val callback: BluetoothLeBroadcast.Callback = object : BluetoothLeBroadcast.Callback {
|
||||
override fun onBroadcastStarted(reason: Int, broadcastId: Int) {}
|
||||
override fun onBroadcastStartFailed(reason: Int) {}
|
||||
override fun onBroadcastStopped(reason: Int, broadcastId: Int) {}
|
||||
override fun onBroadcastStopFailed(reason: Int) {}
|
||||
override fun onPlaybackStarted(reason: Int, broadcastId: Int) {}
|
||||
override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
|
||||
override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {}
|
||||
override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {}
|
||||
override fun onBroadcastMetadataChanged(broadcastId: Int,
|
||||
metadata: BluetoothLeBroadcastMetadata) {}
|
||||
}
|
||||
|
||||
bluetoothLeBroadcast.registerCallback(fakeFgExecutor, callback)
|
||||
return callback;
|
||||
}
|
||||
|
||||
fun setupLeAudioConfiguration(isLeAudio: Boolean) {
|
||||
whenever(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager)
|
||||
whenever(localBluetoothProfileManager.leAudioBroadcastProfile)
|
||||
.thenReturn(localBluetoothLeBroadcast)
|
||||
whenever(localBluetoothLeBroadcast.isEnabled(any())).thenReturn(isLeAudio)
|
||||
whenever(localBluetoothLeBroadcast.appSourceName).thenReturn(BROADCAST_APP_NAME)
|
||||
}
|
||||
|
||||
fun setupBroadcastPackage(currentName: String) {
|
||||
whenever(lmm.packageName).thenReturn(PACKAGE)
|
||||
whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt()))
|
||||
.thenReturn(applicationInfo)
|
||||
whenever(packageManager.getApplicationLabel(applicationInfo)).thenReturn(currentName)
|
||||
context.setMockPackageManager(packageManager)
|
||||
}
|
||||
|
||||
fun captureDeviceData(key: String, oldKey: String? = null): MediaDeviceData {
|
||||
val captor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
|
||||
verify(listener).onMediaDeviceChanged(eq(key), eq(oldKey), captor.capture())
|
||||
|
||||
@@ -20,7 +20,8 @@ class MediaTestUtils {
|
||||
device = null,
|
||||
active = true,
|
||||
resumeAction = null,
|
||||
isPlaying = false,
|
||||
instanceId = InstanceId.fakeInstanceId(-1),
|
||||
appUid = -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user