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

Commit b4c4c362 authored by Tim Peng's avatar Tim Peng
Browse files

Add entry point at output switcher to do group operation

-Entry point is available only when there are more than 1 connected device
-Add group Slice item when it is available
-Add intent filter in manifest
-Add test case

Bug: 146813761
Test: make -j42 RunSettingsRoboTests
Change-Id: If398b7a31219fd1910503d96fe7593622528c792
parent af4d55e7
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -3202,6 +3202,10 @@
                    <action android:name="com.android.settings.panel.action.MEDIA_OUTPUT" />
                    <category android:name="android.intent.category.DEFAULT" />
                </intent-filter>
                <intent-filter>
                    <action android:name="com.android.settings.panel.action.MEDIA_OUTPUT_GROUP" />
                    <category android:name="android.intent.category.DEFAULT" />
                </intent-filter>
        </activity-alias>

        <provider android:name=".slices.SettingsSliceProvider"
+1 −2
Original line number Diff line number Diff line
@@ -118,8 +118,7 @@ public class MediaOutputGroupSlice implements CustomSliceable {
        return listBuilder.build();
    }

    private void addRow(ListBuilder listBuilder, List<MediaDevice> mediaDevices,
            boolean selected) {
    private void addRow(ListBuilder listBuilder, List<MediaDevice> mediaDevices, boolean selected) {
        for (MediaDevice device : mediaDevices) {
            final int maxVolume = device.getMaxVolume();
            final IconCompat titleIcon = Utils.createIconWithDrawable(device.getIcon());
+111 −46
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import com.android.settings.slices.CustomSliceable;
import com.android.settings.slices.SliceBackgroundWorker;
import com.android.settings.slices.SliceBroadcastReceiver;
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.media.MediaOutputSliceConstants;

import java.util.Collection;

@@ -54,6 +55,8 @@ public class MediaOutputSlice implements CustomSliceable {

    private static final String TAG = "MediaOutputSlice";
    private static final String MEDIA_DEVICE_ID = "media_device_id";
    private static final String MEDIA_GROUP_DEVICE = "media_group_device";
    private static final String MEDIA_GROUP_REQUEST = "media_group_request";
    private static final int NON_SLIDER_VALUE = -1;

    public static final String MEDIA_PACKAGE_NAME = "media_package_name";
@@ -86,6 +89,15 @@ public class MediaOutputSlice implements CustomSliceable {

        final Collection<MediaDevice> devices = getMediaDevices();
        final MediaDeviceUpdateWorker worker = getWorker();

        if (worker.getSelectedMediaDevice().size() > 1) {
            // Insert group item to the first when it is available
            listBuilder.addInputRange(getGroupRow());
            // Add all other devices
            for (MediaDevice device : devices) {
                addRow(device, null /* connectedDevice */, listBuilder);
            }
        } else {
            final MediaDevice connectedDevice = worker.getCurrentConnectedMediaDevice();
            final boolean isTouched = worker.getIsTouched();
            // Fix the last top device when user press device to transfer.
@@ -97,24 +109,38 @@ public class MediaOutputSlice implements CustomSliceable {
            }

            for (MediaDevice device : devices) {
            if (topDevice == null
                    || !TextUtils.equals(topDevice.getId(), device.getId())) {
                if (topDevice == null || !TextUtils.equals(topDevice.getId(), device.getId())) {
                    addRow(device, connectedDevice, listBuilder);
                }
            }

        }
        return listBuilder.build();
    }

    private void addRow(MediaDevice device, MediaDevice connectedDevice, ListBuilder listBuilder) {
        if (connectedDevice != null && TextUtils.equals(device.getId(), connectedDevice.getId())) {
            listBuilder.addInputRange(getActiveDeviceHeaderRow(device));
        } else {
            listBuilder.addRow(getMediaDeviceRow(device));
        }
    private ListBuilder.InputRangeBuilder getGroupRow() {
        final IconCompat icon = IconCompat.createWithResource(mContext,
                R.drawable.ic_speaker_group_black_24dp);
        final CharSequence sessionName = getWorker().getSessionName();
        final CharSequence title = TextUtils.isEmpty(sessionName)
                ? mContext.getString(R.string.media_output_group) : sessionName;
        final PendingIntent broadcastAction =
                getBroadcastIntent(mContext, MEDIA_GROUP_DEVICE, MEDIA_GROUP_DEVICE.hashCode());
        final SliceAction primarySliceAction = SliceAction.createDeeplink(broadcastAction, icon,
                ListBuilder.ICON_IMAGE, title);
        final ListBuilder.InputRangeBuilder builder = new ListBuilder.InputRangeBuilder()
                .setTitleItem(icon, ListBuilder.ICON_IMAGE)
                .setTitle(title)
                .setPrimaryAction(primarySliceAction)
                .setInputAction(getSliderInputAction(MEDIA_GROUP_DEVICE.hashCode(),
                        MEDIA_GROUP_DEVICE))
                .setMax(getWorker().getSessionVolumeMax())
                .setValue(getWorker().getSessionVolume())
                .addEndItem(getEndItemSliceAction());
        return builder;
    }

    private ListBuilder.InputRangeBuilder getActiveDeviceHeaderRow(MediaDevice device) {
    private void addRow(MediaDevice device, MediaDevice connectedDevice, ListBuilder listBuilder) {
        if (connectedDevice != null && TextUtils.equals(device.getId(), connectedDevice.getId())) {
            final String title = device.getName();
            final IconCompat icon = getDeviceIconCompat(device);

@@ -122,6 +148,8 @@ public class MediaOutputSlice implements CustomSliceable {
                    getBroadcastIntent(mContext, device.getId(), device.hashCode());
            final SliceAction primarySliceAction = SliceAction.createDeeplink(broadcastAction, icon,
                    ListBuilder.ICON_IMAGE, title);

            if (device.getMaxVolume() > 0) {
                final ListBuilder.InputRangeBuilder builder = new ListBuilder.InputRangeBuilder()
                        .setTitleItem(icon, ListBuilder.ICON_IMAGE)
                        .setTitle(title)
@@ -129,7 +157,24 @@ public class MediaOutputSlice implements CustomSliceable {
                        .setInputAction(getSliderInputAction(device.hashCode(), device.getId()))
                        .setMax(device.getMaxVolume())
                        .setValue(device.getCurrentVolume());
        return builder;
                // Check end item visibility
                if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE
                        && !getWorker().getSelectableMediaDevice().isEmpty()) {
                    builder.addEndItem(getEndItemSliceAction());
                }
                listBuilder.addInputRange(builder);
            } else {
                final ListBuilder.RowBuilder builder = getMediaDeviceRow(device);
                // Check end item visibility
                if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE
                        && !getWorker().getSelectableMediaDevice().isEmpty()) {
                    builder.addEndItem(getEndItemSliceAction());
                }
                listBuilder.addRow(builder);
            }
        } else {
            listBuilder.addRow(getMediaDeviceRow(device));
        }
    }

    private PendingIntent getSliderInputAction(int requestCode, String id) {
@@ -141,6 +186,20 @@ public class MediaOutputSlice implements CustomSliceable {
        return PendingIntent.getBroadcast(mContext, requestCode, intent, 0);
    }

    private SliceAction getEndItemSliceAction() {
        final Intent intent = new Intent()
                .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT_GROUP)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
                        getWorker().getPackageName());

        return SliceAction.createDeeplink(
                PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */),
                IconCompat.createWithResource(mContext, R.drawable.ic_add_blue_24dp),
                ListBuilder.ICON_IMAGE,
                mContext.getText(R.string.add));
    }

    private IconCompat getDeviceIconCompat(MediaDevice device) {
        Drawable drawable = device.getIcon();
        if (drawable == null) {
@@ -169,14 +228,12 @@ public class MediaOutputSlice implements CustomSliceable {
        final PendingIntent broadcastAction =
                getBroadcastIntent(mContext, device.getId(), device.hashCode());
        final IconCompat deviceIcon = getDeviceIconCompat(device);

        final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder()
                .setTitleItem(deviceIcon, ListBuilder.ICON_IMAGE)
                .setPrimaryAction(SliceAction.create(broadcastAction, deviceIcon,
                        ListBuilder.ICON_IMAGE, deviceName));
        // Append status to tile only for the disconnected Bluetooth device.
                .setTitleItem(deviceIcon, ListBuilder.ICON_IMAGE);

        if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
                && !device.isConnected()) {
            // Append status to title only for the disconnected Bluetooth device.
            final SpannableString spannableTitle = new SpannableString(
                    mContext.getString(R.string.media_output_disconnected_status, deviceName));
            spannableTitle.setSpan(new ForegroundColorSpan(Color.GRAY), deviceName.length(),
@@ -214,21 +271,29 @@ public class MediaOutputSlice implements CustomSliceable {
        if (TextUtils.isEmpty(id)) {
            return;
        }

        final int newPosition = intent.getIntExtra(EXTRA_RANGE_VALUE, NON_SLIDER_VALUE);
        if (TextUtils.equals(id, MEDIA_GROUP_DEVICE)) {
            // Session volume adjustment
            worker.adjustSessionVolume(newPosition);
        } else {
            final MediaDevice device = worker.getMediaDeviceById(id);
            if (device == null) {
                Log.d(TAG, "onNotifyChange: Unable to get device " + id);
                return;
            }
        final int newPosition = intent.getIntExtra(EXTRA_RANGE_VALUE, NON_SLIDER_VALUE);

            if (newPosition == NON_SLIDER_VALUE) {
                // Intent for device connection
            Log.d(TAG, "onNotifyChange() device name : " + device.getName());
                Log.d(TAG, "onNotifyChange: Switch to " + device.getName());
                worker.setIsTouched(true);
                worker.connectDevice(device);
            } else {
            // Intent for volume adjustment
                // Single device volume adjustment
                worker.adjustVolume(device, newPosition);
            }
        }
    }

    @Override
    public Intent getIntent() {
+3 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.settings.panel;

import static com.android.settingslib.media.MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT;
import static com.android.settingslib.media.MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT_GROUP;

import android.content.Context;
import android.os.Bundle;
@@ -46,6 +47,8 @@ public class PanelFeatureProviderImpl implements PanelFeatureProvider {
                return WifiPanel.create(context);
            case Settings.Panel.ACTION_VOLUME:
                return VolumePanel.create(context);
            case ACTION_MEDIA_OUTPUT_GROUP:
                return MediaOutputGroupPanel.create(context, mediaPackageName);
        }

        throw new IllegalStateException("No matching panel for: "  + panelType);
+160 −2
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.text.TextUtils;

import androidx.slice.Slice;
import androidx.slice.SliceMetadata;
@@ -67,7 +68,9 @@ import java.util.List;
public class MediaOutputSliceTest {

    private static final String TEST_DEVICE_1_ID = "test_device_1_id";
    private static final String TEST_DEVICE_2_ID = "test_device_2_id";
    private static final String TEST_DEVICE_1_NAME = "test_device_1_name";
    private static final String TEST_DEVICE_2_NAME = "test_device_2_name";
    private static final int TEST_DEVICE_1_ICON =
            com.android.internal.R.drawable.ic_bt_headphones_a2dp;

@@ -98,7 +101,8 @@ public class MediaOutputSliceTest {
        mShadowBluetoothAdapter.setEnabled(true);

        mMediaOutputSlice = new MediaOutputSlice(mContext);
        mMediaDeviceUpdateWorker = new MediaDeviceUpdateWorker(mContext, MEDIA_OUTPUT_SLICE_URI);
        mMediaDeviceUpdateWorker = new MediaDeviceUpdateWorker(mContext,
                MEDIA_OUTPUT_SLICE_URI);
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);
        mMediaDeviceUpdateWorker.mLocalMediaManager = mLocalMediaManager;
        mMediaOutputSlice.init(mMediaDeviceUpdateWorker);
@@ -147,6 +151,19 @@ public class MediaOutputSliceTest {
        when(device.getName()).thenReturn(TEST_DEVICE_1_NAME);
        when(device.getIcon()).thenReturn(mTestDrawable);
        when(device.getMaxVolume()).thenReturn(100);
        when(device.isConnected()).thenReturn(true);
        when(device.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
        when(device.getId()).thenReturn(TEST_DEVICE_1_ID);
        final MediaDevice device2 = mock(MediaDevice.class);
        when(device2.getName()).thenReturn(TEST_DEVICE_2_NAME);
        when(device2.getIcon()).thenReturn(mTestDrawable);
        when(device2.getMaxVolume()).thenReturn(100);
        when(device2.isConnected()).thenReturn(false);
        when(device2.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
        when(device2.getId()).thenReturn(TEST_DEVICE_2_ID);
        mDevices.add(device);
        mDevices.add(device2);
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);
        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(device);

        final Slice mediaSlice = mMediaOutputSlice.getSlice();
@@ -165,8 +182,16 @@ public class MediaOutputSliceTest {
        when(device.getMaxVolume()).thenReturn(100);
        when(device.isConnected()).thenReturn(false);
        when(device.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);

        when(device.getId()).thenReturn(TEST_DEVICE_1_ID);
        final MediaDevice device2 = mock(MediaDevice.class);
        when(device2.getName()).thenReturn(TEST_DEVICE_2_NAME);
        when(device2.getIcon()).thenReturn(mTestDrawable);
        when(device2.getMaxVolume()).thenReturn(100);
        when(device2.isConnected()).thenReturn(false);
        when(device2.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
        when(device2.getId()).thenReturn(TEST_DEVICE_2_ID);
        mDevices.add(device);
        mDevices.add(device2);
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);

        final Slice mediaSlice = mMediaOutputSlice.getSlice();
@@ -177,6 +202,139 @@ public class MediaOutputSliceTest {
                R.string.media_output_disconnected_status, TEST_DEVICE_1_NAME));
    }

    @Test
    public void getSlice_inGroupState_checkSliceSize() {
        final List<MediaDevice> mSelectedDevices = new ArrayList<>();
        final List<MediaDevice> mSelectableDevices = new ArrayList<>();
        mDevices.clear();
        final MediaDevice device = mock(MediaDevice.class);
        when(device.getName()).thenReturn(TEST_DEVICE_1_NAME);
        when(device.getIcon()).thenReturn(mTestDrawable);
        when(device.getMaxVolume()).thenReturn(100);
        when(device.isConnected()).thenReturn(true);
        when(device.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
        when(device.getId()).thenReturn(TEST_DEVICE_1_ID);
        final MediaDevice device2 = mock(MediaDevice.class);
        when(device2.getName()).thenReturn(TEST_DEVICE_2_NAME);
        when(device2.getIcon()).thenReturn(mTestDrawable);
        when(device2.getMaxVolume()).thenReturn(100);
        when(device2.isConnected()).thenReturn(true);
        when(device2.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
        when(device2.getId()).thenReturn(TEST_DEVICE_2_ID);
        mSelectedDevices.add(device);
        mSelectedDevices.add(device2);
        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(device);
        mDevices.add(device);
        mDevices.add(device2);
        when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        when(mLocalMediaManager.getSelectableMediaDevice()).thenReturn(mSelectableDevices);
        when(mMediaDeviceUpdateWorker.getSessionVolumeMax()).thenReturn(100);
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);

        final Slice mediaSlice = mMediaOutputSlice.getSlice();

        assertThat(SliceQuery.findAll(mediaSlice, FORMAT_SLICE, HINT_LIST_ITEM, null).size())
                .isEqualTo(mDevices.size() + 1);
    }

    @Test
    public void getSlice_notInGroupState_checkSliceSize() {
        final List<MediaDevice> mSelectedDevices = new ArrayList<>();
        final List<MediaDevice> mSelectableDevices = new ArrayList<>();
        mDevices.clear();
        final MediaDevice device = mock(MediaDevice.class);
        when(device.getName()).thenReturn(TEST_DEVICE_1_NAME);
        when(device.getIcon()).thenReturn(mTestDrawable);
        when(device.getMaxVolume()).thenReturn(100);
        when(device.isConnected()).thenReturn(true);
        when(device.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
        when(device.getId()).thenReturn(TEST_DEVICE_1_ID);
        final MediaDevice device2 = mock(MediaDevice.class);
        when(device2.getName()).thenReturn(TEST_DEVICE_2_NAME);
        when(device2.getIcon()).thenReturn(mTestDrawable);
        when(device2.getMaxVolume()).thenReturn(100);
        when(device2.isConnected()).thenReturn(true);
        when(device2.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
        when(device2.getId()).thenReturn(TEST_DEVICE_2_ID);
        mSelectedDevices.add(device);
        mSelectableDevices.add(device2);
        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(device);
        mDevices.add(device);
        mDevices.add(device2);
        when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        when(mLocalMediaManager.getSelectableMediaDevice()).thenReturn(mSelectableDevices);
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);

        final Slice mediaSlice = mMediaOutputSlice.getSlice();

        assertThat(SliceQuery.findAll(mediaSlice, FORMAT_SLICE, HINT_LIST_ITEM, null).size())
                .isEqualTo(mDevices.size());
    }

    @Test
    public void getSlice_singleCastDevice_notContainGroupIconText() {
        final List<MediaDevice> mSelectedDevices = new ArrayList<>();
        final List<MediaDevice> mSelectableDevices = new ArrayList<>();
        mDevices.clear();
        final MediaDevice device = mock(MediaDevice.class);
        when(device.getName()).thenReturn(TEST_DEVICE_1_NAME);
        when(device.getIcon()).thenReturn(mTestDrawable);
        when(device.getMaxVolume()).thenReturn(100);
        when(device.isConnected()).thenReturn(true);
        when(device.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
        when(device.getId()).thenReturn(TEST_DEVICE_1_ID);
        when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mDevices);
        when(mLocalMediaManager.getSelectableMediaDevice()).thenReturn(null);
        mSelectedDevices.add(device);
        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(device);
        mDevices.add(device);
        when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        when(mLocalMediaManager.getSelectableMediaDevice()).thenReturn(mSelectableDevices);
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);

        final Slice mediaSlice = mMediaOutputSlice.getSlice();

        final String sliceInfo = SliceQuery.findAll(mediaSlice, FORMAT_SLICE, HINT_LIST_ITEM,
                null).toString();

        assertThat(TextUtils.indexOf(sliceInfo, mContext.getText(R.string.add))).isEqualTo(-1);
    }

    @Test
    public void getSlice_multipleCastDevices_containGroupIconText() {
        final List<MediaDevice> mSelectedDevices = new ArrayList<>();
        final List<MediaDevice> mSelectableDevices = new ArrayList<>();
        mDevices.clear();
        final MediaDevice device = mock(MediaDevice.class);
        when(device.getName()).thenReturn(TEST_DEVICE_1_NAME);
        when(device.getIcon()).thenReturn(mTestDrawable);
        when(device.getMaxVolume()).thenReturn(100);
        when(device.isConnected()).thenReturn(true);
        when(device.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
        when(device.getId()).thenReturn(TEST_DEVICE_1_ID);
        final MediaDevice device2 = mock(MediaDevice.class);
        when(device2.getName()).thenReturn(TEST_DEVICE_2_NAME);
        when(device2.getIcon()).thenReturn(mTestDrawable);
        when(device2.getMaxVolume()).thenReturn(100);
        when(device2.isConnected()).thenReturn(true);
        when(device2.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
        when(device2.getId()).thenReturn(TEST_DEVICE_2_ID);
        mSelectedDevices.add(device);
        mSelectableDevices.add(device2);
        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(device);
        mDevices.add(device);
        mDevices.add(device2);
        when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        when(mLocalMediaManager.getSelectableMediaDevice()).thenReturn(mSelectableDevices);
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);

        final Slice mediaSlice = mMediaOutputSlice.getSlice();
        String sliceInfo = SliceQuery.findAll(mediaSlice, FORMAT_SLICE, HINT_LIST_ITEM,
                null).toString();

        assertThat(TextUtils.indexOf(sliceInfo, mContext.getText(R.string.add))).isNotEqualTo(-1);
    }

    @Test
    public void onNotifyChange_foundMediaDevice_connect() {
        mDevices.clear();