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

Commit c4399504 authored by Alexandr Shabalin's avatar Alexandr Shabalin Committed by Android (Google) Code Review
Browse files

Merge "Move selected items collapsing logic inside MediaSwitchingController." into main

parents 8441f07f 96dbd524
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);