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

Commit d6783aaf authored by Derek Jedral's avatar Derek Jedral Committed by Android (Google) Code Review
Browse files

Merge "Group session MediaItems together in OutputSwitcher" into main

parents ecaa7e92 76a75f86
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