Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 96dbd524 authored by Alex Shabalin's avatar Alex Shabalin
Browse files

Move selected items collapsing logic inside MediaSwitchingController.

- This is needed because the operation should be performed on the
output device list, not the merged output and input device list.
- It wasn't done initially because MediaSwitchingController was used
for the TV Output Switcher. After ag/33429645 this is no longer the
case.

Bug: 432652209
Test: atest MediaOutputAdapterTest MediaSwitchingControllerTest
Flag: EXEMPT refactor
Change-Id: I23ca1080f5a6a27e8f746ea456b5034877e3a55a
parent a2e45a11
Loading
Loading
Loading
Loading
+61 −48
Original line number Diff line number Diff line
@@ -141,11 +141,7 @@ class MediaOutputAdapterTest : SysuiTestCase() {

    @Test
    fun getItemId_forDeviceGroup_returnsItemType() {
        mMediaSwitchingController.stub {
            on { isGroupListCollapsed } doReturn true
            on { isVolumeControlEnabledForSession } doReturn true
        }
        initializeSession()
        initializeGroupSessionCollapsed()

        assertThat(mMediaOutputAdapter.getItemId(1))
            .isEqualTo(MediaItemType.TYPE_DEVICE_GROUP.toLong())
@@ -342,8 +338,8 @@ class MediaOutputAdapterTest : SysuiTestCase() {
        }
        updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))

        // positions: 0 - collapsible drop down, 1 - device1, 2 - device2.
        createAndBindDeviceViewHolder(position = 2).apply {
        // positions: 0 - device1, 1 - device2.
        createAndBindDeviceViewHolder(position = 1).apply {
            assertThat(mGroupButton.visibility).isEqualTo(VISIBLE)
            assertThat(mGroupButton.contentDescription)
                .isEqualTo(
@@ -662,40 +658,25 @@ class MediaOutputAdapterTest : SysuiTestCase() {
            on { isGroupListCollapsed } doReturn true
            on { isVolumeControlEnabledForSession } doReturn true
        }
        initializeSession()

        with(mMediaOutputAdapter) {
            assertThat(itemCount).isEqualTo(2)
            assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_GROUP_DIVIDER)
            assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE_GROUP)
        }
    }
        mMediaItems.add(MediaItem.createGroupDividerMediaItem("Connected Speakers"))
        mMediaItems.add(MediaItem.createDeviceGroupMediaItem())

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING)
    fun multipleSelectedDevices_volumeControlDisabled_notCollapseList() {
        mMediaSwitchingController.stub {
            on { isGroupListCollapsed } doReturn true
            on { isVolumeControlEnabledForSession } doReturn false
        }
        initializeSession()
        mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController)
        mMediaOutputAdapter.updateItems()

        with(mMediaOutputAdapter) {
            assertThat(itemCount).isEqualTo(2)
            assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_DEVICE)
            assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE)
            assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_GROUP_DIVIDER)
            assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE_GROUP)
        }
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING)
    fun multipleSelectedDevices_listCollapsed_verifySessionControl() {
        mMediaSwitchingController.stub {
            on { isGroupListCollapsed } doReturn true
            // TODO: remove once FLAG_ENABLE_OUTPUT_SWITCHER_PERSONAL_AUDIO_SHARING is cleaned up.
            on { isVolumeControlEnabledForSession } doReturn true
        }
        initializeSession()
        mMediaSwitchingController.stub { on { isVolumeControlEnabledForSession } doReturn true }
        initializeGroupSessionCollapsed()

        createAndBindDeviceViewHolder(position = 1).apply {
            assertThat(mTitleText.text.toString()).isEqualTo(TEST_SESSION_NAME)
@@ -725,22 +706,9 @@ class MediaOutputAdapterTest : SysuiTestCase() {
    }

    @Test
    fun multipleSelectedDevices_expandIconClicked_verifyIndividualDevices() {
        mMediaSwitchingController.stub {
            on { isGroupListCollapsed } doReturn true
            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()
    fun multipleSelectedDevices_listExpanded_verifyIndividualDevices() {
        mMediaSwitchingController.stub { on { isVolumeControlEnabledForSession } doReturn true }
        initializeGroupSessionExpanded()

        createAndBindDeviceViewHolder(position = 1).apply {
            assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
@@ -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) =
        ContextThemeWrapper(
            context,
@@ -784,9 +772,34 @@ class MediaOutputAdapterTest : SysuiTestCase() {
        }
    }

    private fun initializeSession() {
        mMediaSwitchingController.stub { on { hasGroupPlayback() } doReturn true }
    private fun initializeGroupSessionCollapsed() {
        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 {
            on { isSelected() } doReturn true
            on { isSelectable() } doReturn true
+0 −45
Original line number Diff line number Diff line
@@ -53,63 +53,18 @@ import com.google.android.material.slider.Slider
/** A RecyclerView adapter for the legacy UI media output dialog device list. */
class MediaOutputAdapter(controller: MediaSwitchingController) :
    MediaOutputAdapterBase(controller) {
    private var mGroupSelectedItems: Boolean? = null // Unset until the first render.

    /** Refreshes the RecyclerView dataset and forces re-render. */
    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 =
            mController.getMediaItemList(false /* addConnectNewDeviceButton */).toMutableList()

        addSeparatorForTheFirstGroupDivider(newList)
        coalesceSelectedDevices(newList)

        mMediaItemList.clear()
        mMediaItemList.addAll(newList)

        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 {
        if (position >= mMediaItemList.size) {
            Log.e(TAG, "Item position exceeds list size: $position")
+51 −3
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import static android.media.RoutingChangeInfo.ENTRY_POINT_SYSTEM_OUTPUT_SWITCHER
import static android.provider.Settings.ACTION_BLUETOOTH_SETTINGS;

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.Notification;
@@ -158,6 +160,7 @@ public class MediaSwitchingController
    private boolean mIsGroupListCollapsed = true;
    private boolean mHasAdjustVolumeUserRestriction = false;
    private long mStartTime;
    @Nullable private Boolean mGroupSelectedItems = null; // Unset until the first render.

    @VisibleForTesting
    final InputRouteManager.InputDeviceCallback mInputDeviceCallback =
@@ -321,6 +324,14 @@ public class MediaSwitchingController
        boolean isListEmpty = mOutputMediaItemListProxy.isEmpty();
        if (isListEmpty || !mIsRefreshing) {
            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();
        } else {
            synchronized (mMediaDevicesLock) {
@@ -638,10 +649,13 @@ public class MediaSwitchingController
    }

    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(
                        false)).count();
        return selectedCount > 1;
                        false)).toList();
    }

    @Nullable
@@ -701,12 +715,46 @@ public class MediaSwitchingController
    private List<MediaItem> getOutputDeviceList(boolean addConnectDeviceButton) {
        List<MediaItem> mediaItems = new ArrayList<>(
                mOutputMediaItemListProxy.getOutputMediaItemList());
        if (enableOutputSwitcherRedesign()) {
            addSeparatorForTheFirstGroupDivider(mediaItems);
            coalesceSelectedDevices(mediaItems);
        }
        if (addConnectDeviceButton) {
            attachConnectNewDeviceItemIfNeeded(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) {
        mediaItems.add(
                MediaItem.createGroupDividerMediaItem(
+147 −1
Original line number Diff line number Diff line
@@ -18,6 +18,10 @@ package com.android.systemui.media.dialog;

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 org.mockito.ArgumentMatchers.any;
@@ -28,6 +32,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.after;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -700,7 +705,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
            if (item.getMediaDevice().isPresent()) {
                devices.add(item.getMediaDevice().get());
            }
            if (item.getMediaItemType() == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) {
            if (item.getMediaItemType() == TYPE_GROUP_DIVIDER) {
                dividerSize++;
            }
        }
@@ -743,6 +748,147 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
        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
    public void onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() {
        mMediaSwitchingController.start(mCb);