Loading packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt +61 −48 Original line number Original line Diff line number Diff line Loading @@ -141,11 +141,7 @@ class MediaOutputAdapterTest : SysuiTestCase() { @Test @Test fun getItemId_forDeviceGroup_returnsItemType() { fun getItemId_forDeviceGroup_returnsItemType() { mMediaSwitchingController.stub { initializeGroupSessionCollapsed() on { isGroupListCollapsed } doReturn true on { isVolumeControlEnabledForSession } doReturn true } initializeSession() assertThat(mMediaOutputAdapter.getItemId(1)) assertThat(mMediaOutputAdapter.getItemId(1)) .isEqualTo(MediaItemType.TYPE_DEVICE_GROUP.toLong()) .isEqualTo(MediaItemType.TYPE_DEVICE_GROUP.toLong()) Loading Loading @@ -342,8 +338,8 @@ class MediaOutputAdapterTest : SysuiTestCase() { } } updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) // positions: 0 - collapsible drop down, 1 - device1, 2 - device2. // positions: 0 - device1, 1 - device2. createAndBindDeviceViewHolder(position = 2).apply { createAndBindDeviceViewHolder(position = 1).apply { assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) assertThat(mGroupButton.contentDescription) assertThat(mGroupButton.contentDescription) .isEqualTo( .isEqualTo( Loading Loading @@ -662,40 +658,25 @@ class MediaOutputAdapterTest : SysuiTestCase() { on { isGroupListCollapsed } doReturn true on { isGroupListCollapsed } doReturn true on { isVolumeControlEnabledForSession } doReturn true on { isVolumeControlEnabledForSession } doReturn true } } initializeSession() with(mMediaOutputAdapter) { mMediaItems.add(MediaItem.createGroupDividerMediaItem("Connected Speakers")) assertThat(itemCount).isEqualTo(2) mMediaItems.add(MediaItem.createDeviceGroupMediaItem()) assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_GROUP_DIVIDER) assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE_GROUP) } } @Test mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController) @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING) mMediaOutputAdapter.updateItems() fun multipleSelectedDevices_volumeControlDisabled_notCollapseList() { mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true on { isVolumeControlEnabledForSession } doReturn false } initializeSession() with(mMediaOutputAdapter) { with(mMediaOutputAdapter) { assertThat(itemCount).isEqualTo(2) assertThat(itemCount).isEqualTo(2) assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_DEVICE) assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_GROUP_DIVIDER) assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE) assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE_GROUP) } } } } @Test @Test @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING) @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING) fun multipleSelectedDevices_listCollapsed_verifySessionControl() { fun multipleSelectedDevices_listCollapsed_verifySessionControl() { mMediaSwitchingController.stub { mMediaSwitchingController.stub { on { isVolumeControlEnabledForSession } doReturn true } on { isGroupListCollapsed } doReturn true initializeGroupSessionCollapsed() // TODO: remove once FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING is cleaned up. on { isVolumeControlEnabledForSession } doReturn true } initializeSession() createAndBindDeviceViewHolder(position = 1).apply { createAndBindDeviceViewHolder(position = 1).apply { assertThat(mTitleText.text.toString()).isEqualTo(TEST_SESSION_NAME) assertThat(mTitleText.text.toString()).isEqualTo(TEST_SESSION_NAME) Loading Loading @@ -725,22 +706,9 @@ class MediaOutputAdapterTest : SysuiTestCase() { } } @Test @Test fun multipleSelectedDevices_expandIconClicked_verifyIndividualDevices() { fun multipleSelectedDevices_listExpanded_verifyIndividualDevices() { mMediaSwitchingController.stub { mMediaSwitchingController.stub { on { isVolumeControlEnabledForSession } doReturn true } on { isGroupListCollapsed } doReturn true initializeGroupSessionExpanded() on { isVolumeControlEnabledForSession } doReturn true } initializeSession() val groupDividerViewHolder = mMediaOutputAdapter.onCreateViewHolder( LinearLayout(mContext), MediaItemType.TYPE_GROUP_DIVIDER, ) as MediaGroupDividerViewHolder mMediaOutputAdapter.onBindViewHolder(groupDividerViewHolder, 0) mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn false } groupDividerViewHolder.mExpandButton.performClick() createAndBindDeviceViewHolder(position = 1).apply { createAndBindDeviceViewHolder(position = 1).apply { assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) Loading @@ -757,6 +725,26 @@ class MediaOutputAdapterTest : SysuiTestCase() { } } } } @Test fun multipleSelectedDevices_expandIconClicked_setGroupListCollapsed() { mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true on { isVolumeControlEnabledForSession } doReturn true } initializeGroupSessionCollapsed() val groupDividerViewHolder = mMediaOutputAdapter.onCreateViewHolder( LinearLayout(mContext), MediaItemType.TYPE_GROUP_DIVIDER, ) as MediaGroupDividerViewHolder mMediaOutputAdapter.onBindViewHolder(groupDividerViewHolder, 0) groupDividerViewHolder.mExpandButton.performClick() verify(mMediaSwitchingController).setGroupListCollapsed(false) } private fun contextWithTheme(context: Context) = private fun contextWithTheme(context: Context) = ContextThemeWrapper( ContextThemeWrapper( context, context, Loading Loading @@ -784,9 +772,34 @@ class MediaOutputAdapterTest : SysuiTestCase() { } } } } private fun initializeSession() { private fun initializeGroupSessionCollapsed() { mMediaSwitchingController.stub { on { hasGroupPlayback() } doReturn true } mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true on { hasGroupPlayback() } doReturn true } mMediaItems.add( MediaItem.createExpandableGroupDividerMediaItem( mContext.getString(R.string.media_output_group_title_connected_speakers) ) ) mMediaItems.add(MediaItem.createDeviceGroupMediaItem()) mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController) mMediaOutputAdapter.updateItems() } private fun initializeGroupSessionExpanded() { mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn false on { hasGroupPlayback() } doReturn true } mMediaItems.add( MediaItem.createExpandableGroupDividerMediaItem( mContext.getString(R.string.media_output_group_title_connected_speakers) ) ) mMediaDevice1.stub { mMediaDevice1.stub { on { isSelected() } doReturn true on { isSelected() } doReturn true on { isSelectable() } doReturn true on { isSelectable() } doReturn true Loading packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt +0 −45 Original line number Original line Diff line number Diff line Loading @@ -53,63 +53,18 @@ import com.google.android.material.slider.Slider /** A RecyclerView adapter for the legacy UI media output dialog device list. */ /** A RecyclerView adapter for the legacy UI media output dialog device list. */ class MediaOutputAdapter(controller: MediaSwitchingController) : class MediaOutputAdapter(controller: MediaSwitchingController) : MediaOutputAdapterBase(controller) { MediaOutputAdapterBase(controller) { private var mGroupSelectedItems: Boolean? = null // Unset until the first render. /** Refreshes the RecyclerView dataset and forces re-render. */ /** Refreshes the RecyclerView dataset and forces re-render. */ override fun updateItems() { override fun updateItems() { if (mGroupSelectedItems == null) { // Decide whether to group devices only during the initial render. // Avoid grouping broadcast devices because grouped volume control is not available for // broadcast session. mGroupSelectedItems = mController.hasGroupPlayback() && (!Flags.enableOutputSwitcherPersonalAudioSharing() || mController.isVolumeControlEnabledForSession) } val newList = val newList = mController.getMediaItemList(false /* addConnectNewDeviceButton */).toMutableList() mController.getMediaItemList(false /* addConnectNewDeviceButton */).toMutableList() addSeparatorForTheFirstGroupDivider(newList) coalesceSelectedDevices(newList) mMediaItemList.clear() mMediaItemList.clear() mMediaItemList.addAll(newList) mMediaItemList.addAll(newList) notifyDataSetChanged() notifyDataSetChanged() } } private fun addSeparatorForTheFirstGroupDivider(newList: MutableList<MediaItem>) { for ((i, item) in newList.withIndex()) { if (item.mediaItemType == TYPE_GROUP_DIVIDER) { newList[i] = MediaItem.createGroupDividerWithSeparatorMediaItem(item.title) break } } } /** * If there are 2+ selected devices, adds an "Connected speakers" expandable group divider and * displays a single session control instead of individual device controls. */ private fun coalesceSelectedDevices(newList: MutableList<MediaItem>) { val selectedDevices = newList.filter { this.isSelectedDevice(it) } if (mGroupSelectedItems == true && selectedDevices.size > 1) { newList.removeAll(selectedDevices.toSet()) if (mController.isGroupListCollapsed) { newList.add(0, MediaItem.createDeviceGroupMediaItem()) } else { newList.addAll(0, selectedDevices) } newList.add(0, mController.connectedSpeakersExpandableGroupDivider) } } private fun isSelectedDevice(mediaItem: MediaItem): Boolean { return mediaItem.mediaDevice.getOrNull()?.isSelected ?: false } override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long { if (position >= mMediaItemList.size) { if (position >= mMediaItemList.size) { Log.e(TAG, "Item position exceeds list size: $position") Log.e(TAG, "Item position exceeds list size: $position") Loading packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +51 −3 Original line number Original line Diff line number Diff line Loading @@ -22,6 +22,8 @@ import static android.media.RoutingChangeInfo.ENTRY_POINT_SYSTEM_OUTPUT_SWITCHER import static android.provider.Settings.ACTION_BLUETOOTH_SETTINGS; import static android.provider.Settings.ACTION_BLUETOOTH_SETTINGS; import static com.android.media.flags.Flags.allowOutputSwitcherListRearrangementWithinTimeout; import static com.android.media.flags.Flags.allowOutputSwitcherListRearrangementWithinTimeout; import static com.android.media.flags.Flags.enableOutputSwitcherRedesign; import static com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_GROUP_DIVIDER; import android.app.KeyguardManager; import android.app.KeyguardManager; import android.app.Notification; import android.app.Notification; Loading Loading @@ -158,6 +160,7 @@ public class MediaSwitchingController private boolean mIsGroupListCollapsed = true; private boolean mIsGroupListCollapsed = true; private boolean mHasAdjustVolumeUserRestriction = false; private boolean mHasAdjustVolumeUserRestriction = false; private long mStartTime; private long mStartTime; @Nullable private Boolean mGroupSelectedItems = null; // Unset until the first render. @VisibleForTesting @VisibleForTesting final InputRouteManager.InputDeviceCallback mInputDeviceCallback = final InputRouteManager.InputDeviceCallback mInputDeviceCallback = Loading Loading @@ -321,6 +324,14 @@ public class MediaSwitchingController boolean isListEmpty = mOutputMediaItemListProxy.isEmpty(); boolean isListEmpty = mOutputMediaItemListProxy.isEmpty(); if (isListEmpty || !mIsRefreshing) { if (isListEmpty || !mIsRefreshing) { buildMediaItems(devices); buildMediaItems(devices); if (mGroupSelectedItems == null) { // Decide whether to group devices only during the initial render. // Avoid grouping broadcast devices because grouped volume control is not // available for broadcast session. mGroupSelectedItems = hasGroupPlayback() && (!Flags.enableOutputSwitcherPersonalAudioSharing() || isVolumeControlEnabledForSession()); } mCallback.onDeviceListChanged(); mCallback.onDeviceListChanged(); } else { } else { synchronized (mMediaDevicesLock) { synchronized (mMediaDevicesLock) { Loading Loading @@ -638,10 +649,13 @@ public class MediaSwitchingController } } boolean hasGroupPlayback() { boolean hasGroupPlayback() { long selectedCount = mOutputMediaItemListProxy.getOutputMediaItemList().stream() return getSelectedDeviceItems().size() > 1; } List<MediaItem> getSelectedDeviceItems() { return mOutputMediaItemListProxy.getOutputMediaItemList().stream() .filter(item -> item.getMediaDevice().map(MediaDevice::isSelected).orElse( .filter(item -> item.getMediaDevice().map(MediaDevice::isSelected).orElse( false)).count(); false)).toList(); return selectedCount > 1; } } @Nullable @Nullable Loading Loading @@ -701,12 +715,46 @@ public class MediaSwitchingController private List<MediaItem> getOutputDeviceList(boolean addConnectDeviceButton) { private List<MediaItem> getOutputDeviceList(boolean addConnectDeviceButton) { List<MediaItem> mediaItems = new ArrayList<>( List<MediaItem> mediaItems = new ArrayList<>( mOutputMediaItemListProxy.getOutputMediaItemList()); mOutputMediaItemListProxy.getOutputMediaItemList()); if (enableOutputSwitcherRedesign()) { addSeparatorForTheFirstGroupDivider(mediaItems); coalesceSelectedDevices(mediaItems); } if (addConnectDeviceButton) { if (addConnectDeviceButton) { attachConnectNewDeviceItemIfNeeded(mediaItems); attachConnectNewDeviceItemIfNeeded(mediaItems); } } return mediaItems; return mediaItems; } } private void addSeparatorForTheFirstGroupDivider(List<MediaItem> outputList) { for (int i = 0; i < outputList.size(); i++) { MediaItem item = outputList.get(i); if (item.getMediaItemType() == TYPE_GROUP_DIVIDER) { outputList.set(i, MediaItem.createGroupDividerWithSeparatorMediaItem(item.getTitle())); break; } } } /** * If there are 2+ selected devices, adds an "Connected speakers" expandable group divider and * displays a single session control instead of individual device controls. */ private void coalesceSelectedDevices(List<MediaItem> outputList) { List<MediaItem> selectedDevices = getSelectedDeviceItems(); if (Boolean.TRUE.equals(mGroupSelectedItems) && hasGroupPlayback()) { outputList.removeAll(selectedDevices); if (isGroupListCollapsed()) { outputList.addFirst(MediaItem.createDeviceGroupMediaItem()); } else { outputList.addAll(0, selectedDevices); } outputList.addFirst(getConnectedSpeakersExpandableGroupDivider()); } } private void addInputDevices(List<MediaItem> mediaItems) { private void addInputDevices(List<MediaItem> mediaItems) { mediaItems.add( mediaItems.add( MediaItem.createGroupDividerMediaItem( MediaItem.createGroupDividerMediaItem( Loading packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java +147 −1 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,10 @@ package com.android.systemui.media.dialog; import static android.media.RoutingChangeInfo.ENTRY_POINT_SYSTEM_OUTPUT_SWITCHER; import static android.media.RoutingChangeInfo.ENTRY_POINT_SYSTEM_OUTPUT_SWITCHER; import static com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE; import static com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE_GROUP; import static com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_GROUP_DIVIDER; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any; Loading @@ -28,6 +32,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.after; import static org.mockito.Mockito.after; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.never; Loading Loading @@ -700,7 +705,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { if (item.getMediaDevice().isPresent()) { if (item.getMediaDevice().isPresent()) { devices.add(item.getMediaDevice().get()); devices.add(item.getMediaDevice().get()); } } if (item.getMediaItemType() == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) { if (item.getMediaItemType() == TYPE_GROUP_DIVIDER) { dividerSize++; dividerSize++; } } } } Loading Loading @@ -743,6 +748,147 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { assertThat(mMediaSwitchingController.hasMutingExpectedDevice()).isFalse(); assertThat(mMediaSwitchingController.hasMutingExpectedDevice()).isFalse(); } } @Test @EnableFlags({ Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN, Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING }) public void onDeviceListUpdate_groupPlaybackAndExpanded_allSelectedDevicesOnTop() { when(mMediaDevice1.isSelected()).thenReturn(true); when(mMediaDevice2.isSelected()).thenReturn(true); mMediaSwitchingController.setGroupListCollapsed(false); doAnswer(invocation -> { LocalMediaManager.DeviceCallback callback = invocation.getArgument(0); callback.onDeviceListUpdate(mMediaDevices); return null; }).when(mLocalMediaManager).registerCallback(any()); doReturn(true).when(mLocalMediaManager).isMediaSessionAvailableForVolumeControl(); mMediaSwitchingController.start(mCb); List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList(); assertThat(resultList.get(0).getMediaItemType()).isEqualTo(TYPE_GROUP_DIVIDER); assertThat(resultList.get(0).getTitle()).isEqualTo( mContext.getString(R.string.media_output_group_title_connected_speakers)); assertThat(resultList.get(0).isExpandableDivider()).isTrue(); assertThat(resultList.get(1).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice1); assertThat(resultList.get(2).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(2).getMediaDevice().get()).isEqualTo(mMediaDevice2); assertThat(resultList.size()).isEqualTo(3); } @Test @EnableFlags({ Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN, Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING }) public void onDeviceListUpdate_groupPlaybackAndCollapsed_groupControlAtTheTop() { when(mMediaDevice1.isSelected()).thenReturn(true); when(mMediaDevice2.isSelected()).thenReturn(true); mMediaSwitchingController.setGroupListCollapsed(true); doAnswer(invocation -> { LocalMediaManager.DeviceCallback callback = invocation.getArgument(0); callback.onDeviceListUpdate(mMediaDevices); return null; }).when(mLocalMediaManager).registerCallback(any()); doReturn(true).when(mLocalMediaManager).isMediaSessionAvailableForVolumeControl(); mMediaSwitchingController.start(mCb); List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList(); assertThat(resultList.get(0).getMediaItemType()).isEqualTo(TYPE_GROUP_DIVIDER); assertThat(resultList.get(0).getTitle()).isEqualTo( mContext.getString(R.string.media_output_group_title_connected_speakers)); assertThat(resultList.get(0).isExpandableDivider()).isTrue(); assertThat(resultList.get(1).getMediaItemType()).isEqualTo(TYPE_DEVICE_GROUP); assertThat(resultList.size()).isEqualTo(2); } @Test @EnableFlags({ Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN, Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING }) public void onDeviceListUpdate_sessionVolumeUnavailable_noGroupControl() { when(mMediaDevice1.isSelected()).thenReturn(true); when(mMediaDevice2.isSelected()).thenReturn(true); mMediaSwitchingController.setGroupListCollapsed(true); doAnswer(invocation -> { LocalMediaManager.DeviceCallback callback = invocation.getArgument(0); callback.onDeviceListUpdate(mMediaDevices); return null; }).when(mLocalMediaManager).registerCallback(any()); doReturn(false).when(mLocalMediaManager).isMediaSessionAvailableForVolumeControl(); mMediaSwitchingController.start(mCb); mMediaSwitchingController.setGroupListCollapsed(true); mMediaSwitchingController.clearMediaItemList(); mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList(); assertThat(resultList.get(0).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(0).getMediaDevice().get()).isEqualTo(mMediaDevice1); assertThat(resultList.get(1).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice2); assertThat(resultList.size()).isEqualTo(2); } @Test @EnableFlags({ Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN, Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING }) public void onDeviceListUpdate_groupPlaybackCreatedLater_noGroupControl() { when(mMediaDevice1.isSelected()).thenReturn(true); when(mMediaDevice2.isSelected()).thenReturn(false); mMediaSwitchingController.setGroupListCollapsed(true); doReturn(false).when(mLocalMediaManager).isMediaSessionAvailableForVolumeControl(); doAnswer(invocation -> { LocalMediaManager.DeviceCallback callback = invocation.getArgument(0); callback.onDeviceListUpdate(mMediaDevices); return null; }).when(mLocalMediaManager).registerCallback(any()); mMediaSwitchingController.start(mCb); // Add second selected device after the initial update. when(mMediaDevice2.isSelected()).thenReturn(true); // Skip 2+ seconds to prevent the list cleanup on refresh. mClock.advanceTime(2500); mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList(); assertThat(resultList.get(0).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(0).getMediaDevice().get()).isEqualTo(mMediaDevice1); assertThat(resultList.get(1).getMediaItemType()).isEqualTo(TYPE_GROUP_DIVIDER); assertThat(resultList.get(1).hasTopSeparator()).isTrue(); assertThat(resultList.get(1).getTitle()).isEqualTo( mContext.getString(R.string.media_output_group_title_speakers_and_displays)); assertThat(resultList.get(2).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(2).getMediaDevice().get()).isEqualTo(mMediaDevice2); assertThat(resultList.size()).isEqualTo(3); } @Test @Test public void onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() { public void onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() { mMediaSwitchingController.start(mCb); mMediaSwitchingController.start(mCb); Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt +61 −48 Original line number Original line Diff line number Diff line Loading @@ -141,11 +141,7 @@ class MediaOutputAdapterTest : SysuiTestCase() { @Test @Test fun getItemId_forDeviceGroup_returnsItemType() { fun getItemId_forDeviceGroup_returnsItemType() { mMediaSwitchingController.stub { initializeGroupSessionCollapsed() on { isGroupListCollapsed } doReturn true on { isVolumeControlEnabledForSession } doReturn true } initializeSession() assertThat(mMediaOutputAdapter.getItemId(1)) assertThat(mMediaOutputAdapter.getItemId(1)) .isEqualTo(MediaItemType.TYPE_DEVICE_GROUP.toLong()) .isEqualTo(MediaItemType.TYPE_DEVICE_GROUP.toLong()) Loading Loading @@ -342,8 +338,8 @@ class MediaOutputAdapterTest : SysuiTestCase() { } } updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) // positions: 0 - collapsible drop down, 1 - device1, 2 - device2. // positions: 0 - device1, 1 - device2. createAndBindDeviceViewHolder(position = 2).apply { createAndBindDeviceViewHolder(position = 1).apply { assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) assertThat(mGroupButton.contentDescription) assertThat(mGroupButton.contentDescription) .isEqualTo( .isEqualTo( Loading Loading @@ -662,40 +658,25 @@ class MediaOutputAdapterTest : SysuiTestCase() { on { isGroupListCollapsed } doReturn true on { isGroupListCollapsed } doReturn true on { isVolumeControlEnabledForSession } doReturn true on { isVolumeControlEnabledForSession } doReturn true } } initializeSession() with(mMediaOutputAdapter) { mMediaItems.add(MediaItem.createGroupDividerMediaItem("Connected Speakers")) assertThat(itemCount).isEqualTo(2) mMediaItems.add(MediaItem.createDeviceGroupMediaItem()) assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_GROUP_DIVIDER) assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE_GROUP) } } @Test mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController) @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING) mMediaOutputAdapter.updateItems() fun multipleSelectedDevices_volumeControlDisabled_notCollapseList() { mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true on { isVolumeControlEnabledForSession } doReturn false } initializeSession() with(mMediaOutputAdapter) { with(mMediaOutputAdapter) { assertThat(itemCount).isEqualTo(2) assertThat(itemCount).isEqualTo(2) assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_DEVICE) assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_GROUP_DIVIDER) assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE) assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE_GROUP) } } } } @Test @Test @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING) @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING) fun multipleSelectedDevices_listCollapsed_verifySessionControl() { fun multipleSelectedDevices_listCollapsed_verifySessionControl() { mMediaSwitchingController.stub { mMediaSwitchingController.stub { on { isVolumeControlEnabledForSession } doReturn true } on { isGroupListCollapsed } doReturn true initializeGroupSessionCollapsed() // TODO: remove once FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING is cleaned up. on { isVolumeControlEnabledForSession } doReturn true } initializeSession() createAndBindDeviceViewHolder(position = 1).apply { createAndBindDeviceViewHolder(position = 1).apply { assertThat(mTitleText.text.toString()).isEqualTo(TEST_SESSION_NAME) assertThat(mTitleText.text.toString()).isEqualTo(TEST_SESSION_NAME) Loading Loading @@ -725,22 +706,9 @@ class MediaOutputAdapterTest : SysuiTestCase() { } } @Test @Test fun multipleSelectedDevices_expandIconClicked_verifyIndividualDevices() { fun multipleSelectedDevices_listExpanded_verifyIndividualDevices() { mMediaSwitchingController.stub { mMediaSwitchingController.stub { on { isVolumeControlEnabledForSession } doReturn true } on { isGroupListCollapsed } doReturn true initializeGroupSessionExpanded() on { isVolumeControlEnabledForSession } doReturn true } initializeSession() val groupDividerViewHolder = mMediaOutputAdapter.onCreateViewHolder( LinearLayout(mContext), MediaItemType.TYPE_GROUP_DIVIDER, ) as MediaGroupDividerViewHolder mMediaOutputAdapter.onBindViewHolder(groupDividerViewHolder, 0) mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn false } groupDividerViewHolder.mExpandButton.performClick() createAndBindDeviceViewHolder(position = 1).apply { createAndBindDeviceViewHolder(position = 1).apply { assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) Loading @@ -757,6 +725,26 @@ class MediaOutputAdapterTest : SysuiTestCase() { } } } } @Test fun multipleSelectedDevices_expandIconClicked_setGroupListCollapsed() { mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true on { isVolumeControlEnabledForSession } doReturn true } initializeGroupSessionCollapsed() val groupDividerViewHolder = mMediaOutputAdapter.onCreateViewHolder( LinearLayout(mContext), MediaItemType.TYPE_GROUP_DIVIDER, ) as MediaGroupDividerViewHolder mMediaOutputAdapter.onBindViewHolder(groupDividerViewHolder, 0) groupDividerViewHolder.mExpandButton.performClick() verify(mMediaSwitchingController).setGroupListCollapsed(false) } private fun contextWithTheme(context: Context) = private fun contextWithTheme(context: Context) = ContextThemeWrapper( ContextThemeWrapper( context, context, Loading Loading @@ -784,9 +772,34 @@ class MediaOutputAdapterTest : SysuiTestCase() { } } } } private fun initializeSession() { private fun initializeGroupSessionCollapsed() { mMediaSwitchingController.stub { on { hasGroupPlayback() } doReturn true } mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true on { hasGroupPlayback() } doReturn true } mMediaItems.add( MediaItem.createExpandableGroupDividerMediaItem( mContext.getString(R.string.media_output_group_title_connected_speakers) ) ) mMediaItems.add(MediaItem.createDeviceGroupMediaItem()) mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController) mMediaOutputAdapter.updateItems() } private fun initializeGroupSessionExpanded() { mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn false on { hasGroupPlayback() } doReturn true } mMediaItems.add( MediaItem.createExpandableGroupDividerMediaItem( mContext.getString(R.string.media_output_group_title_connected_speakers) ) ) mMediaDevice1.stub { mMediaDevice1.stub { on { isSelected() } doReturn true on { isSelected() } doReturn true on { isSelectable() } doReturn true on { isSelectable() } doReturn true Loading
packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt +0 −45 Original line number Original line Diff line number Diff line Loading @@ -53,63 +53,18 @@ import com.google.android.material.slider.Slider /** A RecyclerView adapter for the legacy UI media output dialog device list. */ /** A RecyclerView adapter for the legacy UI media output dialog device list. */ class MediaOutputAdapter(controller: MediaSwitchingController) : class MediaOutputAdapter(controller: MediaSwitchingController) : MediaOutputAdapterBase(controller) { MediaOutputAdapterBase(controller) { private var mGroupSelectedItems: Boolean? = null // Unset until the first render. /** Refreshes the RecyclerView dataset and forces re-render. */ /** Refreshes the RecyclerView dataset and forces re-render. */ override fun updateItems() { override fun updateItems() { if (mGroupSelectedItems == null) { // Decide whether to group devices only during the initial render. // Avoid grouping broadcast devices because grouped volume control is not available for // broadcast session. mGroupSelectedItems = mController.hasGroupPlayback() && (!Flags.enableOutputSwitcherPersonalAudioSharing() || mController.isVolumeControlEnabledForSession) } val newList = val newList = mController.getMediaItemList(false /* addConnectNewDeviceButton */).toMutableList() mController.getMediaItemList(false /* addConnectNewDeviceButton */).toMutableList() addSeparatorForTheFirstGroupDivider(newList) coalesceSelectedDevices(newList) mMediaItemList.clear() mMediaItemList.clear() mMediaItemList.addAll(newList) mMediaItemList.addAll(newList) notifyDataSetChanged() notifyDataSetChanged() } } private fun addSeparatorForTheFirstGroupDivider(newList: MutableList<MediaItem>) { for ((i, item) in newList.withIndex()) { if (item.mediaItemType == TYPE_GROUP_DIVIDER) { newList[i] = MediaItem.createGroupDividerWithSeparatorMediaItem(item.title) break } } } /** * If there are 2+ selected devices, adds an "Connected speakers" expandable group divider and * displays a single session control instead of individual device controls. */ private fun coalesceSelectedDevices(newList: MutableList<MediaItem>) { val selectedDevices = newList.filter { this.isSelectedDevice(it) } if (mGroupSelectedItems == true && selectedDevices.size > 1) { newList.removeAll(selectedDevices.toSet()) if (mController.isGroupListCollapsed) { newList.add(0, MediaItem.createDeviceGroupMediaItem()) } else { newList.addAll(0, selectedDevices) } newList.add(0, mController.connectedSpeakersExpandableGroupDivider) } } private fun isSelectedDevice(mediaItem: MediaItem): Boolean { return mediaItem.mediaDevice.getOrNull()?.isSelected ?: false } override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long { if (position >= mMediaItemList.size) { if (position >= mMediaItemList.size) { Log.e(TAG, "Item position exceeds list size: $position") Log.e(TAG, "Item position exceeds list size: $position") Loading
packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +51 −3 Original line number Original line Diff line number Diff line Loading @@ -22,6 +22,8 @@ import static android.media.RoutingChangeInfo.ENTRY_POINT_SYSTEM_OUTPUT_SWITCHER import static android.provider.Settings.ACTION_BLUETOOTH_SETTINGS; import static android.provider.Settings.ACTION_BLUETOOTH_SETTINGS; import static com.android.media.flags.Flags.allowOutputSwitcherListRearrangementWithinTimeout; import static com.android.media.flags.Flags.allowOutputSwitcherListRearrangementWithinTimeout; import static com.android.media.flags.Flags.enableOutputSwitcherRedesign; import static com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_GROUP_DIVIDER; import android.app.KeyguardManager; import android.app.KeyguardManager; import android.app.Notification; import android.app.Notification; Loading Loading @@ -158,6 +160,7 @@ public class MediaSwitchingController private boolean mIsGroupListCollapsed = true; private boolean mIsGroupListCollapsed = true; private boolean mHasAdjustVolumeUserRestriction = false; private boolean mHasAdjustVolumeUserRestriction = false; private long mStartTime; private long mStartTime; @Nullable private Boolean mGroupSelectedItems = null; // Unset until the first render. @VisibleForTesting @VisibleForTesting final InputRouteManager.InputDeviceCallback mInputDeviceCallback = final InputRouteManager.InputDeviceCallback mInputDeviceCallback = Loading Loading @@ -321,6 +324,14 @@ public class MediaSwitchingController boolean isListEmpty = mOutputMediaItemListProxy.isEmpty(); boolean isListEmpty = mOutputMediaItemListProxy.isEmpty(); if (isListEmpty || !mIsRefreshing) { if (isListEmpty || !mIsRefreshing) { buildMediaItems(devices); buildMediaItems(devices); if (mGroupSelectedItems == null) { // Decide whether to group devices only during the initial render. // Avoid grouping broadcast devices because grouped volume control is not // available for broadcast session. mGroupSelectedItems = hasGroupPlayback() && (!Flags.enableOutputSwitcherPersonalAudioSharing() || isVolumeControlEnabledForSession()); } mCallback.onDeviceListChanged(); mCallback.onDeviceListChanged(); } else { } else { synchronized (mMediaDevicesLock) { synchronized (mMediaDevicesLock) { Loading Loading @@ -638,10 +649,13 @@ public class MediaSwitchingController } } boolean hasGroupPlayback() { boolean hasGroupPlayback() { long selectedCount = mOutputMediaItemListProxy.getOutputMediaItemList().stream() return getSelectedDeviceItems().size() > 1; } List<MediaItem> getSelectedDeviceItems() { return mOutputMediaItemListProxy.getOutputMediaItemList().stream() .filter(item -> item.getMediaDevice().map(MediaDevice::isSelected).orElse( .filter(item -> item.getMediaDevice().map(MediaDevice::isSelected).orElse( false)).count(); false)).toList(); return selectedCount > 1; } } @Nullable @Nullable Loading Loading @@ -701,12 +715,46 @@ public class MediaSwitchingController private List<MediaItem> getOutputDeviceList(boolean addConnectDeviceButton) { private List<MediaItem> getOutputDeviceList(boolean addConnectDeviceButton) { List<MediaItem> mediaItems = new ArrayList<>( List<MediaItem> mediaItems = new ArrayList<>( mOutputMediaItemListProxy.getOutputMediaItemList()); mOutputMediaItemListProxy.getOutputMediaItemList()); if (enableOutputSwitcherRedesign()) { addSeparatorForTheFirstGroupDivider(mediaItems); coalesceSelectedDevices(mediaItems); } if (addConnectDeviceButton) { if (addConnectDeviceButton) { attachConnectNewDeviceItemIfNeeded(mediaItems); attachConnectNewDeviceItemIfNeeded(mediaItems); } } return mediaItems; return mediaItems; } } private void addSeparatorForTheFirstGroupDivider(List<MediaItem> outputList) { for (int i = 0; i < outputList.size(); i++) { MediaItem item = outputList.get(i); if (item.getMediaItemType() == TYPE_GROUP_DIVIDER) { outputList.set(i, MediaItem.createGroupDividerWithSeparatorMediaItem(item.getTitle())); break; } } } /** * If there are 2+ selected devices, adds an "Connected speakers" expandable group divider and * displays a single session control instead of individual device controls. */ private void coalesceSelectedDevices(List<MediaItem> outputList) { List<MediaItem> selectedDevices = getSelectedDeviceItems(); if (Boolean.TRUE.equals(mGroupSelectedItems) && hasGroupPlayback()) { outputList.removeAll(selectedDevices); if (isGroupListCollapsed()) { outputList.addFirst(MediaItem.createDeviceGroupMediaItem()); } else { outputList.addAll(0, selectedDevices); } outputList.addFirst(getConnectedSpeakersExpandableGroupDivider()); } } private void addInputDevices(List<MediaItem> mediaItems) { private void addInputDevices(List<MediaItem> mediaItems) { mediaItems.add( mediaItems.add( MediaItem.createGroupDividerMediaItem( MediaItem.createGroupDividerMediaItem( Loading
packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java +147 −1 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,10 @@ package com.android.systemui.media.dialog; import static android.media.RoutingChangeInfo.ENTRY_POINT_SYSTEM_OUTPUT_SWITCHER; import static android.media.RoutingChangeInfo.ENTRY_POINT_SYSTEM_OUTPUT_SWITCHER; import static com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE; import static com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE_GROUP; import static com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_GROUP_DIVIDER; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any; Loading @@ -28,6 +32,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.after; import static org.mockito.Mockito.after; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.never; Loading Loading @@ -700,7 +705,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { if (item.getMediaDevice().isPresent()) { if (item.getMediaDevice().isPresent()) { devices.add(item.getMediaDevice().get()); devices.add(item.getMediaDevice().get()); } } if (item.getMediaItemType() == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) { if (item.getMediaItemType() == TYPE_GROUP_DIVIDER) { dividerSize++; dividerSize++; } } } } Loading Loading @@ -743,6 +748,147 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { assertThat(mMediaSwitchingController.hasMutingExpectedDevice()).isFalse(); assertThat(mMediaSwitchingController.hasMutingExpectedDevice()).isFalse(); } } @Test @EnableFlags({ Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN, Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING }) public void onDeviceListUpdate_groupPlaybackAndExpanded_allSelectedDevicesOnTop() { when(mMediaDevice1.isSelected()).thenReturn(true); when(mMediaDevice2.isSelected()).thenReturn(true); mMediaSwitchingController.setGroupListCollapsed(false); doAnswer(invocation -> { LocalMediaManager.DeviceCallback callback = invocation.getArgument(0); callback.onDeviceListUpdate(mMediaDevices); return null; }).when(mLocalMediaManager).registerCallback(any()); doReturn(true).when(mLocalMediaManager).isMediaSessionAvailableForVolumeControl(); mMediaSwitchingController.start(mCb); List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList(); assertThat(resultList.get(0).getMediaItemType()).isEqualTo(TYPE_GROUP_DIVIDER); assertThat(resultList.get(0).getTitle()).isEqualTo( mContext.getString(R.string.media_output_group_title_connected_speakers)); assertThat(resultList.get(0).isExpandableDivider()).isTrue(); assertThat(resultList.get(1).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice1); assertThat(resultList.get(2).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(2).getMediaDevice().get()).isEqualTo(mMediaDevice2); assertThat(resultList.size()).isEqualTo(3); } @Test @EnableFlags({ Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN, Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING }) public void onDeviceListUpdate_groupPlaybackAndCollapsed_groupControlAtTheTop() { when(mMediaDevice1.isSelected()).thenReturn(true); when(mMediaDevice2.isSelected()).thenReturn(true); mMediaSwitchingController.setGroupListCollapsed(true); doAnswer(invocation -> { LocalMediaManager.DeviceCallback callback = invocation.getArgument(0); callback.onDeviceListUpdate(mMediaDevices); return null; }).when(mLocalMediaManager).registerCallback(any()); doReturn(true).when(mLocalMediaManager).isMediaSessionAvailableForVolumeControl(); mMediaSwitchingController.start(mCb); List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList(); assertThat(resultList.get(0).getMediaItemType()).isEqualTo(TYPE_GROUP_DIVIDER); assertThat(resultList.get(0).getTitle()).isEqualTo( mContext.getString(R.string.media_output_group_title_connected_speakers)); assertThat(resultList.get(0).isExpandableDivider()).isTrue(); assertThat(resultList.get(1).getMediaItemType()).isEqualTo(TYPE_DEVICE_GROUP); assertThat(resultList.size()).isEqualTo(2); } @Test @EnableFlags({ Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN, Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING }) public void onDeviceListUpdate_sessionVolumeUnavailable_noGroupControl() { when(mMediaDevice1.isSelected()).thenReturn(true); when(mMediaDevice2.isSelected()).thenReturn(true); mMediaSwitchingController.setGroupListCollapsed(true); doAnswer(invocation -> { LocalMediaManager.DeviceCallback callback = invocation.getArgument(0); callback.onDeviceListUpdate(mMediaDevices); return null; }).when(mLocalMediaManager).registerCallback(any()); doReturn(false).when(mLocalMediaManager).isMediaSessionAvailableForVolumeControl(); mMediaSwitchingController.start(mCb); mMediaSwitchingController.setGroupListCollapsed(true); mMediaSwitchingController.clearMediaItemList(); mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList(); assertThat(resultList.get(0).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(0).getMediaDevice().get()).isEqualTo(mMediaDevice1); assertThat(resultList.get(1).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice2); assertThat(resultList.size()).isEqualTo(2); } @Test @EnableFlags({ Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN, Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING }) public void onDeviceListUpdate_groupPlaybackCreatedLater_noGroupControl() { when(mMediaDevice1.isSelected()).thenReturn(true); when(mMediaDevice2.isSelected()).thenReturn(false); mMediaSwitchingController.setGroupListCollapsed(true); doReturn(false).when(mLocalMediaManager).isMediaSessionAvailableForVolumeControl(); doAnswer(invocation -> { LocalMediaManager.DeviceCallback callback = invocation.getArgument(0); callback.onDeviceListUpdate(mMediaDevices); return null; }).when(mLocalMediaManager).registerCallback(any()); mMediaSwitchingController.start(mCb); // Add second selected device after the initial update. when(mMediaDevice2.isSelected()).thenReturn(true); // Skip 2+ seconds to prevent the list cleanup on refresh. mClock.advanceTime(2500); mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList(); assertThat(resultList.get(0).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(0).getMediaDevice().get()).isEqualTo(mMediaDevice1); assertThat(resultList.get(1).getMediaItemType()).isEqualTo(TYPE_GROUP_DIVIDER); assertThat(resultList.get(1).hasTopSeparator()).isTrue(); assertThat(resultList.get(1).getTitle()).isEqualTo( mContext.getString(R.string.media_output_group_title_speakers_and_displays)); assertThat(resultList.get(2).getMediaItemType()).isEqualTo(TYPE_DEVICE); assertThat(resultList.get(2).getMediaDevice().get()).isEqualTo(mMediaDevice2); assertThat(resultList.size()).isEqualTo(3); } @Test @Test public void onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() { public void onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() { mMediaSwitchingController.start(mCb); mMediaSwitchingController.start(mCb); Loading