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(); + } +}