diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index f07627a1a3468..4cd8d19b8121b 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1402,4 +1402,11 @@
@*android:dimen/rounded_corner_radius
@*android:dimen/rounded_corner_radius_top
@*android:dimen/rounded_corner_radius_bottom
+
+
+ 11dp
+ 364dp
+ 52dp
+ 36dp
+ 16dp
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 95de4860ddfa7..824521ecd1e76 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2855,4 +2855,19 @@
Add controls
Edit controls
+
+
+ Add outputs
+
+ Group
+
+ 1 device selected
+
+ %1$d devices selected
+
+ %1$s (disconnected)
+
+ Couldn\'t connect. Try again.
+
+ Pair new device
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
new file mode 100644
index 0000000000000..64d20a273931a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2020 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.dialog;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.MediaMetadata;
+import android.media.RoutingSessionInfo;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.android.settingslib.RestrictedLockUtilsInternal;
+import com.android.settingslib.Utils;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.media.InfoMediaManager;
+import com.android.settingslib.media.LocalMediaManager;
+import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.media.MediaOutputSliceConstants;
+import com.android.settingslib.utils.ThreadUtils;
+import com.android.systemui.R;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.phone.ShadeController;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.inject.Inject;
+
+/**
+ * Controller for media output dialog
+ */
+public class MediaOutputController implements LocalMediaManager.DeviceCallback{
+
+ private static final String TAG = "MediaOutputController";
+ private static final boolean DEBUG = false;
+
+ private final String mPackageName;
+ private final Context mContext;
+ private final MediaSessionManager mMediaSessionManager;
+ private final ShadeController mShadeController;
+ private final ActivityStarter mActivityStarter;
+ @VisibleForTesting
+ final List mMediaDevices = new CopyOnWriteArrayList<>();
+
+ private MediaController mMediaController;
+ @VisibleForTesting
+ Callback mCallback;
+ @VisibleForTesting
+ LocalMediaManager mLocalMediaManager;
+
+ @Inject
+ public MediaOutputController(@NonNull Context context, String packageName,
+ MediaSessionManager mediaSessionManager, LocalBluetoothManager
+ lbm, ShadeController shadeController, ActivityStarter starter) {
+ mContext = context;
+ mPackageName = packageName;
+ mMediaSessionManager = mediaSessionManager;
+ mShadeController = shadeController;
+ mActivityStarter = starter;
+ InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm);
+ mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
+ }
+
+ void start(@NonNull Callback cb) {
+ mMediaDevices.clear();
+ if (!TextUtils.isEmpty(mPackageName)) {
+ for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) {
+ if (TextUtils.equals(controller.getPackageName(), mPackageName)) {
+ mMediaController = controller;
+ mMediaController.unregisterCallback(mCb);
+ mMediaController.registerCallback(mCb);
+ break;
+ }
+ }
+ }
+ if (mMediaController == null) {
+ if (DEBUG) {
+ Log.d(TAG, "No media controller for " + mPackageName);
+ }
+ }
+ if (mLocalMediaManager == null) {
+ if (DEBUG) {
+ Log.d(TAG, "No local media manager " + mPackageName);
+ }
+ return;
+ }
+ mCallback = cb;
+ mLocalMediaManager.unregisterCallback(this);
+ mLocalMediaManager.stopScan();
+ mLocalMediaManager.registerCallback(this);
+ mLocalMediaManager.startScan();
+ }
+
+ void stop() {
+ if (mMediaController != null) {
+ mMediaController.unregisterCallback(mCb);
+ }
+ if (mLocalMediaManager != null) {
+ mLocalMediaManager.unregisterCallback(this);
+ mLocalMediaManager.stopScan();
+ }
+ mMediaDevices.clear();
+ }
+
+ @Override
+ public void onDeviceListUpdate(List devices) {
+ buildMediaDevices(devices);
+ mCallback.onRouteChanged();
+ }
+
+ @Override
+ public void onSelectedDeviceStateChanged(MediaDevice device,
+ @LocalMediaManager.MediaDeviceState int state) {
+ mCallback.onRouteChanged();
+ }
+
+ @Override
+ public void onDeviceAttributesChanged() {
+ mCallback.onRouteChanged();
+ }
+
+ @Override
+ public void onRequestFailed(int reason) {
+ mCallback.onRouteChanged();
+ }
+
+ CharSequence getHeaderTitle() {
+ if (mMediaController != null) {
+ final MediaMetadata metadata = mMediaController.getMetadata();
+ if (metadata != null) {
+ return metadata.getDescription().getTitle();
+ }
+ }
+ return mContext.getText(R.string.controls_media_title);
+ }
+
+ CharSequence getHeaderSubTitle() {
+ if (mMediaController == null) {
+ return null;
+ }
+ final MediaMetadata metadata = mMediaController.getMetadata();
+ if (metadata == null) {
+ return null;
+ }
+ return metadata.getDescription().getSubtitle();
+ }
+
+ IconCompat getHeaderIcon() {
+ if (mMediaController == null) {
+ return null;
+ }
+ final MediaMetadata metadata = mMediaController.getMetadata();
+ if (metadata != null) {
+ final Bitmap bitmap = metadata.getDescription().getIconBitmap();
+ if (bitmap != null) {
+ final Bitmap roundBitmap = Utils.convertCornerRadiusBitmap(mContext, bitmap,
+ (float) mContext.getResources().getDimensionPixelSize(
+ R.dimen.media_output_dialog_icon_corner_radius));
+ return IconCompat.createWithBitmap(roundBitmap);
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Media meta data does not contain icon information");
+ }
+ return getPackageIcon();
+ }
+
+ IconCompat getDeviceIconCompat(MediaDevice device) {
+ Drawable drawable = device.getIcon();
+ if (drawable == null) {
+ if (DEBUG) {
+ Log.d(TAG, "getDeviceIconCompat() device : " + device.getName()
+ + ", drawable is null");
+ }
+ // Use default Bluetooth device icon to handle getIcon() is null case.
+ drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
+ }
+ return BluetoothUtils.createIconWithDrawable(drawable);
+ }
+
+ private IconCompat getPackageIcon() {
+ if (TextUtils.isEmpty(mPackageName)) {
+ return null;
+ }
+ try {
+ final Drawable drawable = mContext.getPackageManager().getApplicationIcon(mPackageName);
+ if (drawable instanceof BitmapDrawable) {
+ return IconCompat.createWithBitmap(((BitmapDrawable) drawable).getBitmap());
+ }
+ final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return IconCompat.createWithBitmap(bitmap);
+ } catch (PackageManager.NameNotFoundException e) {
+ if (DEBUG) {
+ Log.e(TAG, "Package is not found. Unable to get package icon.");
+ }
+ }
+ return null;
+ }
+
+ private void buildMediaDevices(List devices) {
+ // For the first time building list, to make sure the top device is the connected device.
+ if (mMediaDevices.isEmpty()) {
+ final MediaDevice connectedMediaDevice = getCurrentConnectedMediaDevice();
+ if (connectedMediaDevice == null) {
+ if (DEBUG) {
+ Log.d(TAG, "No connected media device.");
+ }
+ mMediaDevices.addAll(devices);
+ return;
+ }
+ for (MediaDevice device : devices) {
+ if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) {
+ mMediaDevices.add(0, device);
+ } else {
+ mMediaDevices.add(device);
+ }
+ }
+ return;
+ }
+ // To keep the same list order
+ final Collection targetMediaDevices = new ArrayList<>();
+ for (MediaDevice originalDevice : mMediaDevices) {
+ for (MediaDevice newDevice : devices) {
+ if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
+ targetMediaDevices.add(newDevice);
+ break;
+ }
+ }
+ }
+ if (targetMediaDevices.size() != devices.size()) {
+ devices.removeAll(targetMediaDevices);
+ targetMediaDevices.addAll(devices);
+ }
+ mMediaDevices.clear();
+ mMediaDevices.addAll(targetMediaDevices);
+ }
+
+ void connectDevice(MediaDevice device) {
+ ThreadUtils.postOnBackgroundThread(() -> {
+ mLocalMediaManager.connectDevice(device);
+ });
+ }
+
+ Collection getMediaDevices() {
+ return mMediaDevices;
+ }
+
+ MediaDevice getCurrentConnectedMediaDevice() {
+ return mLocalMediaManager.getCurrentConnectedDevice();
+ }
+
+ private MediaDevice getMediaDeviceById(String id) {
+ return mLocalMediaManager.getMediaDeviceById(new ArrayList<>(mMediaDevices), id);
+ }
+
+ boolean addDeviceToPlayMedia(MediaDevice device) {
+ return mLocalMediaManager.addDeviceToPlayMedia(device);
+ }
+
+ boolean removeDeviceFromPlayMedia(MediaDevice device) {
+ return mLocalMediaManager.removeDeviceFromPlayMedia(device);
+ }
+
+ List getSelectableMediaDevice() {
+ return mLocalMediaManager.getSelectableMediaDevice();
+ }
+
+ List getSelectedMediaDevice() {
+ return mLocalMediaManager.getSelectedMediaDevice();
+ }
+
+ List getDeselectableMediaDevice() {
+ return mLocalMediaManager.getDeselectableMediaDevice();
+ }
+
+ boolean isDeviceIncluded(Collection deviceCollection, MediaDevice targetDevice) {
+ for (MediaDevice device : deviceCollection) {
+ if (TextUtils.equals(device.getId(), targetDevice.getId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void adjustSessionVolume(String sessionId, int volume) {
+ mLocalMediaManager.adjustSessionVolume(sessionId, volume);
+ }
+
+ void adjustSessionVolume(int volume) {
+ mLocalMediaManager.adjustSessionVolume(volume);
+ }
+
+ int getSessionVolumeMax() {
+ return mLocalMediaManager.getSessionVolumeMax();
+ }
+
+ int getSessionVolume() {
+ return mLocalMediaManager.getSessionVolume();
+ }
+
+ CharSequence getSessionName() {
+ return mLocalMediaManager.getSessionName();
+ }
+
+ void releaseSession() {
+ mLocalMediaManager.releaseSession();
+ }
+
+ List getActiveRemoteMediaDevices() {
+ final List sessionInfos = new ArrayList<>();
+ for (RoutingSessionInfo info : mLocalMediaManager.getActiveMediaSession()) {
+ if (!info.isSystemSession()) {
+ sessionInfos.add(info);
+ }
+ }
+ return sessionInfos;
+ }
+
+ void adjustVolume(MediaDevice device, int volume) {
+ ThreadUtils.postOnBackgroundThread(() -> {
+ device.requestSetVolume(volume);
+ });
+ }
+
+ String getPackageName() {
+ return mPackageName;
+ }
+
+ boolean hasAdjustVolumeUserRestriction() {
+ if (RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
+ mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId()) != null) {
+ return true;
+ }
+ final UserManager um = mContext.getSystemService(UserManager.class);
+ return um.hasBaseUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME,
+ UserHandle.of(UserHandle.myUserId()));
+ }
+
+ boolean isTransferring() {
+ for (MediaDevice device : mMediaDevices) {
+ if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean isZeroMode() {
+ if (mMediaDevices.size() == 1) {
+ final MediaDevice device = mMediaDevices.iterator().next();
+ // Add "pair new" only when local output device exists
+ final int type = device.getDeviceType();
+ if (type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE
+ || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
+ || type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void launchBluetoothPairing() {
+ mCallback.dismissDialog();
+ final ActivityStarter.OnDismissAction postKeyguardAction = () -> {
+ mContext.sendBroadcast(new Intent()
+ .setAction(MediaOutputSliceConstants.ACTION_LAUNCH_BLUETOOTH_PAIRING)
+ .setPackage(MediaOutputSliceConstants.SETTINGS_PACKAGE_NAME));
+ mShadeController.animateCollapsePanels();
+ return true;
+ };
+ mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true);
+ }
+
+ private final MediaController.Callback mCb = new MediaController.Callback() {
+ @Override
+ public void onMetadataChanged(MediaMetadata metadata) {
+ mCallback.onMediaChanged();
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackState playbackState) {
+ final int state = playbackState.getState();
+ if (state == PlaybackState.STATE_STOPPED || state == PlaybackState.STATE_PAUSED) {
+ mCallback.onMediaStoppedOrPaused();
+ }
+ }
+ };
+
+ interface Callback {
+ /**
+ * Override to handle the media content updating.
+ */
+ void onMediaChanged();
+
+ /**
+ * Override to handle the media state updating.
+ */
+ void onMediaStoppedOrPaused();
+
+ /**
+ * Override to handle the device updating.
+ */
+ void onRouteChanged();
+
+ /**
+ * Override to dismiss dialog.
+ */
+ void dismissDialog();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
new file mode 100644
index 0000000000000..0dcdecfdaadb1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2020 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.dialog;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
+import android.media.RoutingSessionInfo;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+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.plugins.ActivityStarter;
+import com.android.systemui.statusbar.phone.ShadeController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class MediaOutputControllerTest extends SysuiTestCase {
+
+ private static final String TEST_PACKAGE_NAME = "com.test.package.name";
+ private static final String TEST_DEVICE_1_ID = "test_device_1_id";
+ private static final String TEST_DEVICE_2_ID = "test_device_2_id";
+ private static final String TEST_ARTIST = "test_artist";
+ private static final String TEST_SONG = "test_song";
+ private static final String TEST_SESSION_ID = "test_session_id";
+ private static final String TEST_SESSION_NAME = "test_session_name";
+ // Mock
+ private MediaController mMediaController = mock(MediaController.class);
+ private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
+ private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager =
+ mock(CachedBluetoothDeviceManager.class);
+ private LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class);
+ private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class);
+ private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
+ private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
+ private MediaMetadata mMediaMetadata = mock(MediaMetadata.class);
+ private RoutingSessionInfo mRemoteSessionInfo = mock(RoutingSessionInfo.class);
+ private ShadeController mShadeController = mock(ShadeController.class);
+ private ActivityStarter mStarter = mock(ActivityStarter.class);
+
+ private Context mSpyContext;
+ private MediaOutputController mMediaOutputController;
+ private LocalMediaManager mLocalMediaManager;
+ private List mMediaControllers = new ArrayList<>();
+ private List mMediaDevices = new ArrayList<>();
+ private MediaDescription mMediaDescription;
+ private List mRoutingSessionInfos = new ArrayList<>();
+
+ @Before
+ public void setUp() {
+ mSpyContext = spy(mContext);
+ when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
+ mMediaControllers.add(mMediaController);
+ when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers);
+ doReturn(mMediaSessionManager).when(mSpyContext).getSystemService(
+ MediaSessionManager.class);
+ when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(
+ mCachedBluetoothDeviceManager);
+ mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME,
+ mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter);
+ mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
+ mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
+ MediaDescription.Builder builder = new MediaDescription.Builder();
+ builder.setTitle(TEST_SONG);
+ builder.setSubtitle(TEST_ARTIST);
+ mMediaDescription = builder.build();
+ when(mMediaMetadata.getDescription()).thenReturn(mMediaDescription);
+ when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_1_ID);
+ when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
+ mMediaDevices.add(mMediaDevice1);
+ mMediaDevices.add(mMediaDevice2);
+ }
+
+ @Test
+ public void start_verifyLocalMediaManagerInit() {
+ mMediaOutputController.start(mCb);
+
+ verify(mLocalMediaManager).registerCallback(mMediaOutputController);
+ verify(mLocalMediaManager).startScan();
+ }
+
+ @Test
+ public void stop_verifyLocalMediaManagerDeinit() {
+ mMediaOutputController.start(mCb);
+ reset(mLocalMediaManager);
+
+ mMediaOutputController.stop();
+
+ verify(mLocalMediaManager).unregisterCallback(mMediaOutputController);
+ verify(mLocalMediaManager).stopScan();
+ }
+
+ @Test
+ public void start_withPackageName_verifyMediaControllerInit() {
+ mMediaOutputController.start(mCb);
+
+ verify(mMediaController).registerCallback(any());
+ }
+
+ @Test
+ public void start_withoutPackageName_verifyMediaControllerInit() {
+ mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager,
+ mLocalBluetoothManager, mShadeController, mStarter);
+
+ mMediaOutputController.start(mCb);
+
+ verify(mMediaController, never()).registerCallback(any());
+ }
+
+ @Test
+ public void stop_withPackageName_verifyMediaControllerDeinit() {
+ mMediaOutputController.start(mCb);
+ reset(mMediaController);
+
+ mMediaOutputController.stop();
+
+ verify(mMediaController).unregisterCallback(any());
+ }
+
+ @Test
+ public void stop_withoutPackageName_verifyMediaControllerDeinit() {
+ mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager,
+ mLocalBluetoothManager, mShadeController, mStarter);
+ mMediaOutputController.start(mCb);
+
+ mMediaOutputController.stop();
+
+ verify(mMediaController, never()).unregisterCallback(any());
+ }
+
+ @Test
+ public void onDeviceListUpdate_verifyDeviceListCallback() {
+ mMediaOutputController.start(mCb);
+ reset(mCb);
+
+ mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ final List devices = new ArrayList<>(mMediaOutputController.getMediaDevices());
+
+ assertThat(devices.containsAll(mMediaDevices)).isTrue();
+ assertThat(devices.size()).isEqualTo(mMediaDevices.size());
+ verify(mCb).onRouteChanged();
+ }
+
+ @Test
+ public void onSelectedDeviceStateChanged_verifyCallback() {
+ mMediaOutputController.start(mCb);
+ reset(mCb);
+
+ mMediaOutputController.onSelectedDeviceStateChanged(mMediaDevice1,
+ LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
+
+ verify(mCb).onRouteChanged();
+ }
+
+ @Test
+ public void onDeviceAttributesChanged_verifyCallback() {
+ mMediaOutputController.start(mCb);
+ reset(mCb);
+
+ mMediaOutputController.onDeviceAttributesChanged();
+
+ verify(mCb).onRouteChanged();
+ }
+
+ @Test
+ public void onRequestFailed_verifyCallback() {
+ mMediaOutputController.start(mCb);
+ reset(mCb);
+
+ mMediaOutputController.onRequestFailed(0 /* reason */);
+
+ verify(mCb).onRouteChanged();
+ }
+
+ @Test
+ public void getHeaderTitle_withoutMetadata_returnDefaultString() {
+ when(mMediaController.getMetadata()).thenReturn(null);
+
+ mMediaOutputController.start(mCb);
+
+ assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(
+ mContext.getText(R.string.controls_media_title));
+ }
+
+ @Test
+ public void getHeaderTitle_withMetadata_returnSongName() {
+ when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
+
+ mMediaOutputController.start(mCb);
+
+ assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(TEST_SONG);
+ }
+
+ @Test
+ public void getHeaderSubTitle_withoutMetadata_returnNull() {
+ when(mMediaController.getMetadata()).thenReturn(null);
+
+ mMediaOutputController.start(mCb);
+
+ assertThat(mMediaOutputController.getHeaderSubTitle()).isNull();
+ }
+
+ @Test
+ public void getHeaderSubTitle_withMetadata_returnArtistName() {
+ when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
+
+ mMediaOutputController.start(mCb);
+
+ assertThat(mMediaOutputController.getHeaderSubTitle()).isEqualTo(TEST_ARTIST);
+ }
+
+ @Test
+ public void connectDevice_verifyConnect() {
+ mMediaOutputController.connectDevice(mMediaDevice1);
+
+ // Wait for background thread execution
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ verify(mLocalMediaManager).connectDevice(mMediaDevice1);
+ }
+
+ @Test
+ public void getActiveRemoteMediaDevice_isSystemSession_returnSession() {
+ when(mRemoteSessionInfo.getId()).thenReturn(TEST_SESSION_ID);
+ when(mRemoteSessionInfo.getName()).thenReturn(TEST_SESSION_NAME);
+ when(mRemoteSessionInfo.getVolumeMax()).thenReturn(100);
+ when(mRemoteSessionInfo.getVolume()).thenReturn(10);
+ when(mRemoteSessionInfo.isSystemSession()).thenReturn(false);
+ mRoutingSessionInfos.add(mRemoteSessionInfo);
+ when(mLocalMediaManager.getActiveMediaSession()).thenReturn(mRoutingSessionInfos);
+
+ assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).containsExactly(
+ mRemoteSessionInfo);
+ }
+
+ @Test
+ public void getActiveRemoteMediaDevice_notSystemSession_returnEmpty() {
+ when(mRemoteSessionInfo.getId()).thenReturn(TEST_SESSION_ID);
+ when(mRemoteSessionInfo.getName()).thenReturn(TEST_SESSION_NAME);
+ when(mRemoteSessionInfo.getVolumeMax()).thenReturn(100);
+ when(mRemoteSessionInfo.getVolume()).thenReturn(10);
+ when(mRemoteSessionInfo.isSystemSession()).thenReturn(true);
+ mRoutingSessionInfos.add(mRemoteSessionInfo);
+ when(mLocalMediaManager.getActiveMediaSession()).thenReturn(mRoutingSessionInfos);
+
+ assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).isEmpty();
+ }
+
+ @Test
+ public void isZeroMode_onlyFromPhoneOutput_returnTrue() {
+ // Multiple available devices
+ assertThat(mMediaOutputController.isZeroMode()).isFalse();
+ when(mMediaDevice1.getDeviceType()).thenReturn(
+ MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE);
+ mMediaDevices.clear();
+ mMediaDevices.add(mMediaDevice1);
+ mMediaOutputController.start(mCb);
+ mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+ assertThat(mMediaOutputController.isZeroMode()).isTrue();
+
+ when(mMediaDevice1.getDeviceType()).thenReturn(
+ MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE);
+
+ assertThat(mMediaOutputController.isZeroMode()).isTrue();
+
+ when(mMediaDevice1.getDeviceType()).thenReturn(
+ MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE);
+
+ assertThat(mMediaOutputController.isZeroMode()).isTrue();
+ }
+
+ @Test
+ public void isZeroMode_notFromPhoneOutput_returnFalse() {
+ when(mMediaDevice1.getDeviceType()).thenReturn(
+ MediaDevice.MediaDeviceType.TYPE_UNKNOWN);
+ mMediaDevices.clear();
+ mMediaDevices.add(mMediaDevice1);
+ mMediaOutputController.start(mCb);
+ mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+ assertThat(mMediaOutputController.isZeroMode()).isFalse();
+
+ when(mMediaDevice1.getDeviceType()).thenReturn(
+ MediaDevice.MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE);
+
+ assertThat(mMediaOutputController.isZeroMode()).isFalse();
+
+ when(mMediaDevice1.getDeviceType()).thenReturn(
+ MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
+
+ assertThat(mMediaOutputController.isZeroMode()).isFalse();
+
+ when(mMediaDevice1.getDeviceType()).thenReturn(
+ MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
+
+ assertThat(mMediaOutputController.isZeroMode()).isFalse();
+
+ when(mMediaDevice1.getDeviceType()).thenReturn(
+ MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE);
+
+ assertThat(mMediaOutputController.isZeroMode()).isFalse();
+ }
+}