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

Commit 76a75f86 authored by Derek Jedral's avatar Derek Jedral
Browse files

Group session MediaItems together in OutputSwitcher

If multiple routes are selected, they will be initially grouped
together, with a carat that allows expansion. The volume seekbar and
icon controls the sesion volume, and the name of the entry is the
session name.

There must already be an existing session before opening the output
switcher for them to be grouped. If a device is added to the session, it
will maintain its existing position until the output switcher is
reopened.

Bug: 388347018
Test: Tested locally, atest
Flag: com.android.media.flags.enable_output_switcher_session_grouping
Change-Id: I7763783ddf4cf66d35dbe34a5f7620fa82ead7cc
parent 8ee3a094
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -124,6 +124,13 @@ flag {
    bug: "185136506"
}

flag {
    name: "enable_output_switcher_session_grouping"
    namespace: "media_better_together"
    description: "Enables selected items in Output Switcher to be grouped together."
    bug: "388347018"
}

flag {
    name: "enable_prevention_of_keep_alive_route_providers"
    namespace: "media_solutions"
+44 −11
Original line number Diff line number Diff line
@@ -658,12 +658,9 @@ public abstract class InfoMediaManager {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            RouteListingPreference routeListingPreference = getRouteListingPreference();
            if (routeListingPreference != null) {
                final List<RouteListingPreference.Item> preferenceRouteListing =
                        Api34Impl.composePreferenceRouteListing(
                                routeListingPreference);
                availableRoutes = Api34Impl.arrangeRouteListByPreference(selectedRoutes,
                        getAvailableRoutesFromRouter(),
                                preferenceRouteListing);
                        routeListingPreference);
            }
            return Api34Impl.filterDuplicatedIds(availableRoutes);
        } else {
@@ -760,11 +757,15 @@ public abstract class InfoMediaManager {
        @DoNotInline
        static List<RouteListingPreference.Item> composePreferenceRouteListing(
                RouteListingPreference routeListingPreference) {
            boolean preferRouteListingOrdering =
                    com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping()
                    && preferRouteListingOrdering(routeListingPreference);
            List<RouteListingPreference.Item> finalizedItemList = new ArrayList<>();
            List<RouteListingPreference.Item> itemList = routeListingPreference.getItems();
            for (RouteListingPreference.Item item : itemList) {
                // Put suggested devices on the top first before further organization
                if ((item.getFlags() & RouteListingPreference.Item.FLAG_SUGGESTED) != 0) {
                if (!preferRouteListingOrdering
                        && (item.getFlags() & RouteListingPreference.Item.FLAG_SUGGESTED) != 0) {
                    finalizedItemList.add(0, item);
                } else {
                    finalizedItemList.add(item);
@@ -792,7 +793,7 @@ public abstract class InfoMediaManager {
         * Returns an ordered list of available devices based on the provided {@code
         * routeListingPreferenceItems}.
         *
         * <p>The result has the following order:
         * <p>The resulting order if enableOutputSwitcherSessionGrouping is disabled is:
         *
         * <ol>
         *   <li>Selected routes.
@@ -800,23 +801,55 @@ public abstract class InfoMediaManager {
         *   <li>Not-selected, non-system, available routes sorted by route listing preference.
         * </ol>
         *
         * <p>The resulting order if enableOutputSwitcherSessionGrouping is enabled is:
         *
         * <ol>
         *   <li>Selected routes sorted by route listing preference.
         *   <li>Selected routes not defined by route listing preference.
         *   <li>Not-selected system routes.
         *   <li>Not-selected, non-system, available routes sorted by route listing preference.
         * </ol>
         *
         *
         * @param selectedRoutes List of currently selected routes.
         * @param availableRoutes List of available routes that match the app's requested route
         *     features.
         * @param routeListingPreferenceItems Ordered list of {@link RouteListingPreference.Item} to
         *     sort routes with.
         * @param routeListingPreference Preferences provided by the app to determine route order.
         */
        @DoNotInline
        static List<MediaRoute2Info> arrangeRouteListByPreference(
                List<MediaRoute2Info> selectedRoutes,
                List<MediaRoute2Info> availableRoutes,
                List<RouteListingPreference.Item> routeListingPreferenceItems) {
                RouteListingPreference routeListingPreference) {
            final List<RouteListingPreference.Item> routeListingPreferenceItems =
                    Api34Impl.composePreferenceRouteListing(routeListingPreference);

            Set<String> sortedRouteIds = new LinkedHashSet<>();

            boolean addSelectedRlpItemsFirst =
                    com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping()
                    && preferRouteListingOrdering(routeListingPreference);
            Set<String> selectedRouteIds = new HashSet<>();

            if (addSelectedRlpItemsFirst) {
                // Add selected RLP items first
                for (MediaRoute2Info selectedRoute : selectedRoutes) {
                    selectedRouteIds.add(selectedRoute.getId());
                }
                for (RouteListingPreference.Item item: routeListingPreferenceItems) {
                    if (selectedRouteIds.contains(item.getRouteId())) {
                        sortedRouteIds.add(item.getRouteId());
                    }
                }
            }

            // Add selected routes first.
            if (com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping()
                    && sortedRouteIds.size() != selectedRoutes.size()) {
                for (MediaRoute2Info selectedRoute : selectedRoutes) {
                    sortedRouteIds.add(selectedRoute.getId());
                }
            }

            // Add not-yet-added system routes.
            for (MediaRoute2Info availableRoute : availableRoutes) {
+78 −4
Original line number Diff line number Diff line
@@ -48,15 +48,20 @@ import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
import android.media.session.MediaSessionManager;
import android.os.Build;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;

import com.android.media.flags.Flags;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.InfoMediaManager.Api34Impl;
import com.android.settingslib.testutils.shadow.ShadowRouter2Manager;

import com.google.common.collect.ImmutableList;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -122,6 +127,8 @@ public class InfoMediaManagerTest {
                    .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO)
                    .build();

    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    @Mock
    private MediaRouter2Manager mRouterManager;
    @Mock
@@ -377,21 +384,26 @@ public class InfoMediaManagerTest {
    }

    private RouteListingPreference setUpPreferenceList(String packageName) {
        return setUpPreferenceList(packageName, false);
    }

    private RouteListingPreference setUpPreferenceList(
                String packageName, boolean useSystemOrdering) {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT",
                Build.VERSION_CODES.UPSIDE_DOWN_CAKE);
        final List<RouteListingPreference.Item> preferenceItemList = new ArrayList<>();
        RouteListingPreference.Item item1 =
        RouteListingPreference.Item item1 = new RouteListingPreference.Item.Builder(
                TEST_ID_3).build();
        RouteListingPreference.Item item2 =
                new RouteListingPreference.Item.Builder(TEST_ID_4)
                        .setFlags(RouteListingPreference.Item.FLAG_SUGGESTED)
                        .build();
        RouteListingPreference.Item item2 = new RouteListingPreference.Item.Builder(
                TEST_ID_3).build();
        preferenceItemList.add(item1);
        preferenceItemList.add(item2);

        RouteListingPreference routeListingPreference =
                new RouteListingPreference.Builder().setItems(
                        preferenceItemList).setUseSystemOrdering(false).build();
                        preferenceItemList).setUseSystemOrdering(useSystemOrdering).build();
        when(mRouterManager.getRouteListingPreference(packageName))
                .thenReturn(routeListingPreference);
        return routeListingPreference;
@@ -908,4 +920,66 @@ public class InfoMediaManagerTest {
        assertThat(device.getState()).isEqualTo(STATE_SELECTED);
        assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(device);
    }

    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
    @Test
    public void composePreferenceRouteListing_useSystemOrderingIsFalse() {
        RouteListingPreference routeListingPreference =
                setUpPreferenceList(TEST_PACKAGE_NAME, false);

        List<RouteListingPreference.Item> routeOrder =
                Api34Impl.composePreferenceRouteListing(routeListingPreference);

        assertThat(routeOrder.get(0).getRouteId()).isEqualTo(TEST_ID_3);
        assertThat(routeOrder.get(1).getRouteId()).isEqualTo(TEST_ID_4);
    }

    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
    @Test
    public void composePreferenceRouteListing_useSystemOrderingIsTrue() {
        RouteListingPreference routeListingPreference =
                setUpPreferenceList(TEST_PACKAGE_NAME, true);

        List<RouteListingPreference.Item> routeOrder =
                Api34Impl.composePreferenceRouteListing(routeListingPreference);

        assertThat(routeOrder.get(0).getRouteId()).isEqualTo(TEST_ID_4);
        assertThat(routeOrder.get(1).getRouteId()).isEqualTo(TEST_ID_3);
    }

    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
    @Test
    public void arrangeRouteListByPreference_useSystemOrderingIsFalse() {
        RouteListingPreference routeListingPreference =
                setUpPreferenceList(TEST_PACKAGE_NAME, false);
        List<MediaRoute2Info> routes = setAvailableRoutesList(TEST_PACKAGE_NAME);
        when(mRouterManager.getSelectedRoutes(any())).thenReturn(routes);

        List<MediaRoute2Info> routeOrder =
                Api34Impl.arrangeRouteListByPreference(
                        routes, routes, routeListingPreference);

        assertThat(routeOrder.get(0).getId()).isEqualTo(TEST_ID_3);
        assertThat(routeOrder.get(1).getId()).isEqualTo(TEST_ID_4);
        assertThat(routeOrder.get(2).getId()).isEqualTo(TEST_ID_2);
        assertThat(routeOrder.get(3).getId()).isEqualTo(TEST_ID_1);
    }

    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
    @Test
    public void arrangeRouteListByPreference_useSystemOrderingIsTrue() {
        RouteListingPreference routeListingPreference =
                setUpPreferenceList(TEST_PACKAGE_NAME, true);
        List<MediaRoute2Info> routes = setAvailableRoutesList(TEST_PACKAGE_NAME);
        when(mRouterManager.getSelectedRoutes(any())).thenReturn(routes);

        List<MediaRoute2Info> routeOrder =
                Api34Impl.arrangeRouteListByPreference(
                        routes, routes, routeListingPreference);

        assertThat(routeOrder.get(0).getId()).isEqualTo(TEST_ID_2);
        assertThat(routeOrder.get(1).getId()).isEqualTo(TEST_ID_3);
        assertThat(routeOrder.get(2).getId()).isEqualTo(TEST_ID_4);
        assertThat(routeOrder.get(3).getId()).isEqualTo(TEST_ID_1);
    }
}
+118 −2
Original line number Diff line number Diff line
@@ -117,8 +117,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
                LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
        mMediaDevices.add(mMediaDevice1);
        mMediaDevices.add(mMediaDevice2);
        mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1));
        mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2));
        mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1, true));
        mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2, false));

        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
        mMediaOutputAdapter.updateItems();
@@ -779,4 +779,120 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
                mViewHolder.getDrawableId(false /* isInputDevice */, false /* isMutedVolumeIcon */))
                .isEqualTo(R.drawable.media_output_icon_volume);
    }

    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
    @Test
    public void multipleSelectedDevices_verifySessionView() {
        initializeSession();

        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                .onCreateViewHolder(
                        new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);

        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
        assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mEndClickIcon.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_SESSION_NAME);
        assertThat(mViewHolder.mSeekBar.getVolume()).isEqualTo(TEST_CURRENT_VOLUME);
    }

    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
    @Test
    public void multipleSelectedDevices_verifyCollapsedView() {
        initializeSession();

        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                .onCreateViewHolder(
                        new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);

        assertThat(mViewHolder.mItemLayout.getVisibility()).isEqualTo(View.GONE);
        assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.GONE);
    }

    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
    @Test
    public void multipleSelectedDevices_expandIconClicked_verifyInitialView() {
        initializeSession();
        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                .onCreateViewHolder(
                        new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);

        mViewHolder.mEndTouchArea.performClick();
        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                .onCreateViewHolder(
                        new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);

        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mEndClickIcon.getVisibility()).isEqualTo(View.GONE);
        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
    }

    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
    @Test
    public void multipleSelectedDevices_expandIconClicked_verifyCollapsedView() {
        initializeSession();
        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                .onCreateViewHolder(
                        new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);

        mViewHolder.mEndTouchArea.performClick();
        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                .onCreateViewHolder(
                        new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);

        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mEndClickIcon.getVisibility()).isEqualTo(View.GONE);
        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
    }

    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
    @Test
    public void deviceCanNotBeDeselected_verifyView() {
        List<MediaDevice> selectedDevices = new ArrayList<>();
        selectedDevices.add(mMediaDevice1);
        when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectedDevices);
        when(mMediaSwitchingController.getSelectedMediaDevice()).thenReturn(selectedDevices);
        when(mMediaSwitchingController.getDeselectableMediaDevice()).thenReturn(new ArrayList<>());

        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                .onCreateViewHolder(
                        new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);

        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
        assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.GONE);
        assertThat(mViewHolder.mEndClickIcon.getVisibility()).isEqualTo(View.GONE);
        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
    }

    private void initializeSession() {
        when(mMediaSwitchingController.getSessionVolumeMax()).thenReturn(TEST_MAX_VOLUME);
        when(mMediaSwitchingController.getSessionVolume()).thenReturn(TEST_CURRENT_VOLUME);
        when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME);

        List<MediaDevice> selectedDevices = new ArrayList<>();
        selectedDevices.add(mMediaDevice1);
        selectedDevices.add(mMediaDevice2);
        when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectedDevices);
        when(mMediaSwitchingController.getSelectedMediaDevice()).thenReturn(selectedDevices);
        when(mMediaSwitchingController.getDeselectableMediaDevice()).thenReturn(selectedDevices);

        mMediaOutputAdapter.updateItems();
    }
}
+26 −0
Original line number Diff line number Diff line
<!--
  ~ Copyright (C) 2025 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.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24"
    android:tint="?attr/colorControlNormal">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M12,15.4 L6,9.4l1.4,-1.4 4.6,4.6 4.6,-4.6 1.4,1.4 -6,6Z" />
</vector>
Loading