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

Commit 09ff25cd authored by tim peng's avatar tim peng Committed by Android (Google) Code Review
Browse files

Merge "Add slice for dynamic grouping in output switcher" into rvc-dev

parents d6f421d5 7dfd41f6
Loading
Loading
Loading
Loading
+32 −0
Original line number Original line Diff line number Diff line
@@ -146,6 +146,34 @@ public class MediaDeviceUpdateWorker extends SliceBackgroundWorker
        return mTopDevice;
        return mTopDevice;
    }
    }


    boolean addDeviceToPlayMedia(MediaDevice device) {
        return mLocalMediaManager.addDeviceToPlayMedia(device);
    }

    boolean removeDeviceFromPlayMedia(MediaDevice device) {
        return mLocalMediaManager.removeDeviceFromPlayMedia(device);
    }

    List<MediaDevice> getSelectableMediaDevice() {
        return mLocalMediaManager.getSelectableMediaDevice();
    }

    List<MediaDevice> getSelectedMediaDevice() {
        return mLocalMediaManager.getSelectedMediaDevice();
    }

    void adjustSessionVolume(int volume) {
        mLocalMediaManager.adjustSessionVolume(volume);
    }

    int getSessionVolumeMax() {
        return mLocalMediaManager.getSessionVolumeMax();
    }

    int getSessionVolume() {
        return mLocalMediaManager.getSessionVolume();
    }

    /**
    /**
     * Find the active MediaDevice.
     * Find the active MediaDevice.
     *
     *
@@ -170,6 +198,10 @@ public class MediaDeviceUpdateWorker extends SliceBackgroundWorker
        });
        });
    }
    }


    String getPackageName() {
        return mPackageName;
    }

    private class DevicesChangedBroadcastReceiver extends BroadcastReceiver {
    private class DevicesChangedBroadcastReceiver extends BroadcastReceiver {
        @Override
        @Override
        public void onReceive(Context context, Intent intent) {
        public void onReceive(Context context, Intent intent) {
+254 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2020 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.
 */

package com.android.settings.media;

import static android.app.slice.Slice.EXTRA_RANGE_VALUE;

import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_GROUP_SLICE_URI;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import androidx.slice.Slice;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.SliceAction;

import com.android.settings.R;
import com.android.settings.Utils;
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 java.util.List;

/**
 * Show the Media device that can be transfer the media.
 */
public class MediaOutputGroupSlice implements CustomSliceable {

    @VisibleForTesting
    static final String GROUP_DEVICES = "group_devices";
    @VisibleForTesting
    static final String MEDIA_DEVICE_ID = "media_device_id";
    @VisibleForTesting
    static final String CUSTOMIZED_ACTION = "customized_action";
    @VisibleForTesting
    static final int ACTION_VOLUME_ADJUSTMENT = 1;
    @VisibleForTesting
    static final int ACTION_MEDIA_SESSION_OPERATION = 2;
    @VisibleForTesting
    static final int ERROR = -1;

    private static final String TAG = "MediaOutputGroupSlice";
    private static final int COLOR_DISABLED = (int) (255 * 0.3);

    private final Context mContext;
    private MediaDeviceUpdateWorker mWorker;

    public MediaOutputGroupSlice(Context context) {
        mContext = context;
    }

    @Override
    public Slice getSlice() {
        // Reload theme for switching dark mode on/off
        mContext.getTheme().applyStyle(R.style.Theme_Settings_Home, true /* force */);
        final ListBuilder listBuilder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY)
                .setAccentColor(COLOR_NOT_TINTED);
        // Add "Group" row
        final IconCompat titleIcon = IconCompat.createWithResource(mContext,
                R.drawable.ic_speaker_group_black_24dp);
        final Bitmap emptyBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
        final int maxVolume = getWorker().getSessionVolumeMax();
        final String title = mContext.getString(R.string.media_output_group);
        final SliceAction primaryAction = SliceAction.createDeeplink(
                getBroadcastIntent(GROUP_DEVICES,
                        GROUP_DEVICES.hashCode(),
                        ACTION_MEDIA_SESSION_OPERATION),
                titleIcon, ListBuilder.ICON_IMAGE, GROUP_DEVICES);
        final SliceAction endItemAction = SliceAction.createDeeplink(
                getBroadcastIntent(GROUP_DEVICES,
                        GROUP_DEVICES.hashCode() + ACTION_MEDIA_SESSION_OPERATION,
                        ACTION_MEDIA_SESSION_OPERATION),
                IconCompat.createWithBitmap(emptyBitmap), ListBuilder.ICON_IMAGE, "");
        if (maxVolume > 0) {    // Add InputRange row
            listBuilder.addInputRange(new ListBuilder.InputRangeBuilder()
                    .setTitleItem(titleIcon, ListBuilder.ICON_IMAGE)
                    .addEndItem(endItemAction)
                    .setTitle(title)
                    .setPrimaryAction(primaryAction)
                    .setInputAction(getBroadcastIntent(GROUP_DEVICES,
                            GROUP_DEVICES.hashCode() + ACTION_VOLUME_ADJUSTMENT,
                            ACTION_VOLUME_ADJUSTMENT))
                    .setMax(maxVolume)
                    .setValue(getWorker().getSessionVolume()));
        } else {    // No max volume information. Add generic Row
            listBuilder.addRow(new ListBuilder.RowBuilder()
                    .setTitleItem(titleIcon, ListBuilder.ICON_IMAGE)
                    .setTitle(title)
                    .setPrimaryAction(primaryAction));
        }
        // Add device row
        addRow(listBuilder, getWorker().getSelectedMediaDevice(), true);
        addRow(listBuilder, getWorker().getSelectableMediaDevice(), false);
        return listBuilder.build();
    }

    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());
            final String title = device.getName();
            final SliceAction disabledIconSliceAction = SliceAction.createDeeplink(
                    getBroadcastIntent(null, 0, 0),
                    getDisabledCheckboxIcon(), ListBuilder.ICON_IMAGE, "");
            final SliceAction enabledIconSliceAction = SliceAction.createToggle(
                    getBroadcastIntent(device.getId(),
                            device.hashCode() + ACTION_MEDIA_SESSION_OPERATION,
                            ACTION_MEDIA_SESSION_OPERATION),
                    IconCompat.createWithResource(mContext, R.drawable.ic_check_box_anim),
                    "",
                    selected);
            if (maxVolume > 0) {    // Add InputRange row
                final ListBuilder.InputRangeBuilder builder = new ListBuilder.InputRangeBuilder()
                        .setTitleItem(titleIcon, ListBuilder.ICON_IMAGE)
                        .setTitle(title)
                        .setInputAction(getBroadcastIntent(device.getId(),
                                device.hashCode() + ACTION_VOLUME_ADJUSTMENT,
                                ACTION_VOLUME_ADJUSTMENT))
                        .setMax(device.getMaxVolume())
                        .setValue(device.getCurrentVolume());
                // Add endItem with different icons
                if (mediaDevices.size() == 1 && selected) {
                    builder.addEndItem(disabledIconSliceAction);
                } else {
                    builder.addEndItem(enabledIconSliceAction);
                }
                listBuilder.addInputRange(builder);
            } else {    // No max volume information. Add generic Row
                final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder()
                        .setTitleItem(titleIcon, ListBuilder.ICON_IMAGE)
                        .setTitle(title);
                // Add endItem with different icons
                if (mediaDevices.size() == 1 && selected) {
                    rowBuilder.addEndItem(disabledIconSliceAction);
                } else {
                    rowBuilder.addEndItem(enabledIconSliceAction);
                }
                listBuilder.addRow(rowBuilder);
            }
        }
    }

    private IconCompat getDisabledCheckboxIcon() {
        final Drawable drawable = mContext.getDrawable(R.drawable.ic_check_box_blue_24dp);
        final Bitmap checkbox = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
                drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(checkbox);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.setAlpha(COLOR_DISABLED);
        drawable.draw(canvas);

        return IconCompat.createWithBitmap(checkbox);
    }

    private PendingIntent getBroadcastIntent(String id, int requestCode, int action) {
        final Intent intent = new Intent(getUri().toString());
        intent.setClass(mContext, SliceBroadcastReceiver.class);
        intent.putExtra(MEDIA_DEVICE_ID, id);
        intent.putExtra(CUSTOMIZED_ACTION, action);
        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        return PendingIntent.getBroadcast(mContext, requestCode, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
    }

    private MediaDeviceUpdateWorker getWorker() {
        if (mWorker == null) {
            mWorker = SliceBackgroundWorker.getInstance(getUri());
        }
        return mWorker;
    }

    @Override
    public Uri getUri() {
        return MEDIA_OUTPUT_GROUP_SLICE_URI;
    }

    @Override
    public void onNotifyChange(Intent intent) {
        final String id = intent.getStringExtra(MEDIA_DEVICE_ID);
        if (TextUtils.isEmpty(id)) {
            Log.e(TAG, "Unable to handle notification. The device is unavailable");
            return;
        }
        final MediaDevice device = getWorker().getMediaDeviceById(id);
        switch (intent.getIntExtra(CUSTOMIZED_ACTION, ERROR)) {
            case ACTION_VOLUME_ADJUSTMENT:
                final int newPosition = intent.getIntExtra(EXTRA_RANGE_VALUE, ERROR);
                if (newPosition == ERROR) {
                    Log.e(TAG, "Unable to adjust volume. The volume value is unavailable");
                    return;
                }
                // Group volume adjustment
                if (TextUtils.equals(id, GROUP_DEVICES)) {
                    getWorker().adjustSessionVolume(newPosition);
                } else {
                    if (device == null) {
                        Log.e(TAG, "Unable to adjust volume. The device(" + id
                                + ") is unavailable");
                        return;
                    }
                    // Single device volume adjustment
                    getWorker().adjustVolume(device, newPosition);
                }
                break;
            case ACTION_MEDIA_SESSION_OPERATION:
                if (device == null) {
                    Log.e(TAG, "Unable to adjust session volume. The device(" + id
                            + ") is unavailable");
                    return;
                }
                if (TextUtils.equals(device.getClientPackageName(), getWorker().getPackageName())) {
                    getWorker().removeDeviceFromPlayMedia(device);
                } else {
                    getWorker().addDeviceToPlayMedia(device);
                }
                break;
        }
    }

    @Override
    public Intent getIntent() {
        return null;
    }

    @Override
    public Class getBackgroundWorkerClass() {
        return MediaDeviceUpdateWorker.class;
    }
}
+270 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2020 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.
 */

package com.android.settings.media;

import static android.app.slice.Slice.EXTRA_RANGE_VALUE;
import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.SliceItem.FORMAT_SLICE;

import static com.android.settings.media.MediaOutputGroupSlice.ACTION_MEDIA_SESSION_OPERATION;
import static com.android.settings.media.MediaOutputGroupSlice.ACTION_VOLUME_ADJUSTMENT;
import static com.android.settings.media.MediaOutputGroupSlice.CUSTOMIZED_ACTION;
import static com.android.settings.media.MediaOutputGroupSlice.GROUP_DEVICES;
import static com.android.settings.media.MediaOutputGroupSlice.MEDIA_DEVICE_ID;
import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_GROUP_SLICE_URI;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;

import androidx.slice.Slice;
import androidx.slice.SliceMetadata;
import androidx.slice.SliceProvider;
import androidx.slice.core.SliceAction;
import androidx.slice.core.SliceQuery;
import androidx.slice.widget.SliceLiveData;

import com.android.settings.R;
import com.android.settings.slices.SliceBackgroundWorker;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

import java.util.ArrayList;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
@Config(shadows = MediaOutputGroupSliceTest.ShadowSliceBackgroundWorker.class)
public class MediaOutputGroupSliceTest {

    private static final String TEST_PACKAGE_NAME = "com.test.music";
    private static final String TEST_PACKAGE_NAME2 = "com.test.music2";
    private static final String TEST_DEVICE_1_ID = "test_device_1_id";
    private static final String TEST_DEVICE_1_NAME = "test_device_1_name";
    private static final String TEST_DEVICE_2_ID = "test_device_2_id";
    private static final String TEST_DEVICE_2_NAME = "test_device_2_name";
    private static final int TEST_VOLUME = 3;

    private static MediaDeviceUpdateWorker sMediaDeviceUpdateWorker;

    @Mock
    private LocalMediaManager mLocalMediaManager;
    @Mock
    private MediaDevice mDevice1;
    @Mock
    private MediaDevice mDevice2;

    private final List<MediaDevice> mSelectableDevices = new ArrayList<>();
    private final List<MediaDevice> mSelectedDevices = new ArrayList<>();

    private Context mContext;
    private MediaOutputGroupSlice mMediaOutputGroupSlice;
    private Drawable mDrawable;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mContext = spy(RuntimeEnvironment.application);

        // Set-up specs for SliceMetadata.
        SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);

        mMediaOutputGroupSlice = new MediaOutputGroupSlice(mContext);
        sMediaDeviceUpdateWorker = spy(new MediaDeviceUpdateWorker(mContext,
                MEDIA_OUTPUT_GROUP_SLICE_URI));
        sMediaDeviceUpdateWorker.mLocalMediaManager = mLocalMediaManager;
        when(sMediaDeviceUpdateWorker.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
        mDrawable = mContext.getDrawable(R.drawable.ic_check_box_blue_24dp);
        when(sMediaDeviceUpdateWorker.getSelectableMediaDevice()).thenReturn(mSelectableDevices);
        when(mDevice1.getId()).thenReturn(TEST_DEVICE_1_ID);
        when(mDevice1.getIcon()).thenReturn(mDrawable);
        when(mDevice1.getName()).thenReturn(TEST_DEVICE_1_NAME);
        when(mDevice1.getMaxVolume()).thenReturn(100);
        when(mDevice1.getCurrentVolume()).thenReturn(10);
        when(mDevice1.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME);
        when(mDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
        when(mDevice2.getIcon()).thenReturn(mDrawable);
        when(mDevice2.getName()).thenReturn(TEST_DEVICE_2_NAME);
        when(mDevice2.getMaxVolume()).thenReturn(100);
        when(mDevice2.getCurrentVolume()).thenReturn(20);
    }

    @Test
    public void getSlice_noMatchedDevice_doNothing() {
        mSelectableDevices.add(mDevice1);
        mSelectedDevices.add(mDevice1);
        when(mLocalMediaManager.getMediaDeviceById(mSelectableDevices, TEST_DEVICE_1_ID))
                .thenReturn(mDevice1);
        sMediaDeviceUpdateWorker.onDeviceListUpdate(mSelectableDevices);
        when(sMediaDeviceUpdateWorker.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        final Intent intent = new Intent();
        intent.putExtra(EXTRA_RANGE_VALUE, TEST_VOLUME);
        intent.putExtra(MEDIA_DEVICE_ID, TEST_DEVICE_2_ID);
        intent.putExtra(CUSTOMIZED_ACTION, ACTION_VOLUME_ADJUSTMENT);

        mMediaOutputGroupSlice.onNotifyChange(intent);

        verify(sMediaDeviceUpdateWorker, never()).adjustSessionVolume(anyInt());
        verify(mDevice1, never()).requestSetVolume(TEST_VOLUME);
    }

    @Test
    public void getSlice_withOneSelectableDevice_checkRowNumber() {
        mSelectableDevices.add(mDevice1);
        mSelectedDevices.add(mDevice2);
        when(sMediaDeviceUpdateWorker.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        when(sMediaDeviceUpdateWorker.getSelectableMediaDevice()).thenReturn(mSelectableDevices);
        final Slice slice = mMediaOutputGroupSlice.getSlice();
        final int rows = SliceQuery.findAll(slice, FORMAT_SLICE, HINT_LIST_ITEM, null).size();

        // Group item and 2 * InputRange
        assertThat(rows).isEqualTo(3);
    }

    @Test
    public void getSlice_withOneSelectableDevice_checkTitle() {
        mSelectableDevices.add(mDevice1);
        mSelectedDevices.add(mDevice1);
        sMediaDeviceUpdateWorker.onDeviceListUpdate(mSelectableDevices);
        when(sMediaDeviceUpdateWorker.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        final Slice slice = mMediaOutputGroupSlice.getSlice();
        final SliceMetadata metadata = SliceMetadata.from(mContext, slice);
        final SliceAction primaryAction = metadata.getPrimaryAction();

        assertThat(primaryAction.getTitle().toString()).isEqualTo(GROUP_DEVICES);
    }

    @Test
    public void onNotifyChange_verifyAdjustDeviceVolume() {
        mSelectableDevices.add(mDevice1);
        mSelectedDevices.add(mDevice1);
        when(mLocalMediaManager.getMediaDeviceById(mSelectableDevices, TEST_DEVICE_1_ID))
                .thenReturn(mDevice1);
        sMediaDeviceUpdateWorker.onDeviceListUpdate(mSelectableDevices);
        when(sMediaDeviceUpdateWorker.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        final Intent intent = new Intent();
        intent.putExtra(EXTRA_RANGE_VALUE, TEST_VOLUME);
        intent.putExtra(MEDIA_DEVICE_ID, TEST_DEVICE_1_ID);
        intent.putExtra(CUSTOMIZED_ACTION, ACTION_VOLUME_ADJUSTMENT);

        mMediaOutputGroupSlice.onNotifyChange(intent);

        verify(mDevice1).requestSetVolume(TEST_VOLUME);
    }

    @Test
    public void onNotifyChange_verifyAdjustGroupVolume() {
        mSelectableDevices.add(mDevice1);
        mSelectedDevices.add(mDevice1);
        when(mLocalMediaManager.getMediaDeviceById(mSelectableDevices, TEST_DEVICE_1_ID))
                .thenReturn(mDevice1);
        sMediaDeviceUpdateWorker.onDeviceListUpdate(mSelectableDevices);
        when(sMediaDeviceUpdateWorker.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        final Intent intent = new Intent();
        intent.putExtra(EXTRA_RANGE_VALUE, TEST_VOLUME);
        intent.putExtra(MEDIA_DEVICE_ID, GROUP_DEVICES);
        intent.putExtra(CUSTOMIZED_ACTION, ACTION_VOLUME_ADJUSTMENT);

        mMediaOutputGroupSlice.onNotifyChange(intent);

        verify(sMediaDeviceUpdateWorker).adjustSessionVolume(TEST_VOLUME);
    }

    @Test
    public void onNotifyChange_sessionOperation_differentClient_verifyAddSession() {
        mSelectableDevices.add(mDevice1);
        mSelectableDevices.add(mDevice2);
        mSelectedDevices.add(mDevice1);
        when(mDevice2.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME2);
        when(mLocalMediaManager.getMediaDeviceById(mSelectableDevices, TEST_DEVICE_2_ID))
                .thenReturn(mDevice2);
        sMediaDeviceUpdateWorker.onDeviceListUpdate(mSelectableDevices);
        when(sMediaDeviceUpdateWorker.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        final Intent intent = new Intent();
        intent.putExtra(MEDIA_DEVICE_ID, TEST_DEVICE_2_ID);
        intent.putExtra(CUSTOMIZED_ACTION, ACTION_MEDIA_SESSION_OPERATION);

        mMediaOutputGroupSlice.onNotifyChange(intent);

        verify(sMediaDeviceUpdateWorker).addDeviceToPlayMedia(mDevice2);
    }

    @Test
    public void onNotifyChange_sessionOperation_sameClient_verifyRemoveSession() {
        mSelectableDevices.add(mDevice1);
        mSelectableDevices.add(mDevice2);
        mSelectedDevices.add(mDevice1);
        when(mDevice2.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME);
        when(mLocalMediaManager.getMediaDeviceById(mSelectableDevices, TEST_DEVICE_2_ID))
                .thenReturn(mDevice2);
        sMediaDeviceUpdateWorker.onDeviceListUpdate(mSelectableDevices);
        when(sMediaDeviceUpdateWorker.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        final Intent intent = new Intent();
        intent.putExtra(MEDIA_DEVICE_ID, TEST_DEVICE_2_ID);
        intent.putExtra(CUSTOMIZED_ACTION, ACTION_MEDIA_SESSION_OPERATION);

        mMediaOutputGroupSlice.onNotifyChange(intent);

        verify(sMediaDeviceUpdateWorker).removeDeviceFromPlayMedia(mDevice2);
    }

    @Test
    public void onNotifyChange_noId_doNothing() {
        mSelectableDevices.add(mDevice1);
        mSelectedDevices.add(mDevice1);
        when(mLocalMediaManager.getMediaDeviceById(mSelectableDevices, TEST_DEVICE_1_ID))
                .thenReturn(mDevice1);
        sMediaDeviceUpdateWorker.onDeviceListUpdate(mSelectableDevices);
        when(sMediaDeviceUpdateWorker.getSelectedMediaDevice()).thenReturn(mSelectedDevices);
        final Intent intent = new Intent();
        intent.putExtra(EXTRA_RANGE_VALUE, TEST_VOLUME);
        intent.putExtra(CUSTOMIZED_ACTION, ACTION_VOLUME_ADJUSTMENT);

        mMediaOutputGroupSlice.onNotifyChange(intent);

        verify(sMediaDeviceUpdateWorker, never()).adjustSessionVolume(anyInt());
        verify(mDevice1, never()).requestSetVolume(TEST_VOLUME);
    }

    @Implements(SliceBackgroundWorker.class)
    public static class ShadowSliceBackgroundWorker {

        @Implementation
        public static SliceBackgroundWorker getInstance(Uri uri) {
            return sMediaDeviceUpdateWorker;
        }
    }
}