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

Commit b266fa60 authored by timhypeng's avatar timhypeng
Browse files

Add remote media slice in volume panel

-Add test cases

Bug: 142772656
Test: make -j42 RunSettingsRoboTests
Change-Id: I62d3054a4343ed2c7fbb0b4d7aeb5a48da194b02
parent cf4e12bb
Loading
Loading
Loading
Loading
+11 −0
Original line number Original line Diff line number Diff line
@@ -146,6 +146,17 @@ public class MediaDeviceUpdateWorker extends SliceBackgroundWorker
        return mTopDevice;
        return mTopDevice;
    }
    }


    /**
     * Find the active MediaDevice.
     *
     * @param type the media device type.
     * @return MediaDevice list
     *
     */
    public List<MediaDevice> getActiveMediaDevice(@MediaDevice.MediaDeviceType int type) {
        return mLocalMediaManager.getActiveMediaDevice(type);
    }

    /**
    /**
     * Request to set volume.
     * Request to set volume.
     *
     *
+182 −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.REMOTE_MEDIA_SLICE_URI;

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

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

import com.android.settings.R;
import com.android.settings.SubSettings;
import com.android.settings.notification.SoundSettings;
import com.android.settings.slices.CustomSliceable;
import com.android.settings.slices.SliceBackgroundWorker;
import com.android.settings.slices.SliceBroadcastReceiver;
import com.android.settings.slices.SliceBuilderUtils;
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.media.MediaOutputSliceConstants;

import java.util.List;

/**
 * Display the Remote Media device information.
 */
public class RemoteMediaSlice implements CustomSliceable {

    private static final String TAG = "RemoteMediaSlice";
    private static final String MEDIA_ID = "media_id";

    private final Context mContext;

    private MediaDeviceUpdateWorker mWorker;

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

    @Override
    public void onNotifyChange(Intent intent) {
        final int newPosition = intent.getIntExtra(EXTRA_RANGE_VALUE, -1);
        final String id = intent.getStringExtra(MEDIA_ID);
        if (!TextUtils.isEmpty(id)) {
            getWorker().adjustVolume(getWorker().getMediaDeviceById(id), newPosition);
        }
    }

    @Override
    public Slice getSlice() {
        final ListBuilder listBuilder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY)
                .setAccentColor(COLOR_NOT_TINTED);
        if (getWorker() == null) {
            Log.e(TAG, "Unable to get the slice worker.");
            return listBuilder.build();
        }
        // Only displaying remote devices
        final List<MediaDevice> mediaDevices = getWorker().getActiveMediaDevice(
                MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
        if (mediaDevices.isEmpty()) {
            Log.d(TAG, "No active remote media device");
            return listBuilder.build();
        }
        final CharSequence castVolume = mContext.getText(R.string.remote_media_volume_option_title);
        final CharSequence outputTitle = mContext.getText(R.string.media_output_title);
        final IconCompat icon = IconCompat.createWithResource(mContext,
                R.drawable.ic_volume_remote);
        // To create an empty icon to indent the row
        final IconCompat emptyIcon = createEmptyIcon();
        int requestCode = 0;
        for (MediaDevice mediaDevice : mediaDevices) {
            final int maxVolume = mediaDevice.getMaxVolume();
            if (maxVolume <= 0) {
                Log.d(TAG, "Unable to add Slice. " + mediaDevice.getName() + ": max volume is "
                        + maxVolume);
                continue;
            }
            final String title = castVolume + " (" + mediaDevice.getClientAppLabel() + ")";
            listBuilder.addInputRange(new InputRangeBuilder()
                    .setTitleItem(icon, ListBuilder.ICON_IMAGE)
                    .setTitle(title)
                    .setInputAction(getSliderInputAction(requestCode++, mediaDevice.getId()))
                    .setPrimaryAction(getSoundSettingAction(title, icon, mediaDevice.getId()))
                    .setMax(maxVolume)
                    .setValue(mediaDevice.getCurrentVolume()));
            listBuilder.addRow(new ListBuilder.RowBuilder()
                    .setTitle(outputTitle)
                    .setSubtitle(mediaDevice.getName())
                    .setTitleItem(emptyIcon, ListBuilder.ICON_IMAGE)
                    .setPrimaryAction(getMediaOutputSliceAction()));
        }
        return listBuilder.build();
    }

    private IconCompat createEmptyIcon() {
        final Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
        return IconCompat.createWithBitmap(bitmap);
    }

    private PendingIntent getSliderInputAction(int requestCode, String id) {
        final Intent intent = new Intent(getUri().toString())
                .setData(getUri())
                .putExtra(MEDIA_ID, id)
                .setClass(mContext, SliceBroadcastReceiver.class);
        return PendingIntent.getBroadcast(mContext, requestCode, intent, 0);
    }

    private SliceAction getSoundSettingAction(String actionTitle, IconCompat icon, String id) {
        final Uri contentUri = new Uri.Builder().appendPath(id).build();
        final Intent intent = SliceBuilderUtils.buildSearchResultPageIntent(mContext,
                SoundSettings.class.getName(),
                id,
                mContext.getText(R.string.sound_settings).toString(), 0);
        intent.setClassName(mContext.getPackageName(), SubSettings.class.getName());
        intent.setData(contentUri);
        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
        final SliceAction primarySliceAction = SliceAction.createDeeplink(pendingIntent, icon,
                ListBuilder.ICON_IMAGE, actionTitle);
        return primarySliceAction;
    }

    private SliceAction getMediaOutputSliceAction() {
        final Intent intent = new Intent()
                .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        final IconCompat icon = IconCompat.createWithResource(mContext,
                R.drawable.ic_volume_remote);
        final PendingIntent primaryActionIntent = PendingIntent.getActivity(mContext,
                0 /* requestCode */, intent, 0 /* flags */);
        final SliceAction primarySliceAction = SliceAction.createDeeplink(
                primaryActionIntent, icon, ListBuilder.ICON_IMAGE,
                mContext.getText(R.string.media_output_title));
        return primarySliceAction;
    }

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

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

    @Override
    public Class getBackgroundWorkerClass() {
        return MediaDeviceUpdateWorker.class;
    }

    private MediaDeviceUpdateWorker getWorker() {
        if (mWorker == null) {
            mWorker = SliceBackgroundWorker.getInstance(getUri());
        }
        return mWorker;
    }
}
+3 −5
Original line number Original line Diff line number Diff line
@@ -17,10 +17,10 @@
package com.android.settings.panel;
package com.android.settings.panel;


import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI;
import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI;
import static com.android.settings.slices.CustomSliceRegistry.REMOTE_MEDIA_SLICE_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_ALARM_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_ALARM_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_CALL_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_CALL_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_MEDIA_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_MEDIA_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_REMOTE_MEDIA_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_RINGER_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_RINGER_URI;


import android.app.settings.SettingsEnums;
import android.app.settings.SettingsEnums;
@@ -30,7 +30,6 @@ import android.net.Uri;
import android.provider.Settings;
import android.provider.Settings;


import com.android.settings.R;
import com.android.settings.R;
import com.android.settings.notification.RemoteVolumePreferenceController;


import java.util.ArrayList;
import java.util.ArrayList;
import java.util.List;
import java.util.List;
@@ -55,9 +54,8 @@ public class VolumePanel implements PanelContent {
    @Override
    @Override
    public List<Uri> getSlices() {
    public List<Uri> getSlices() {
        final List<Uri> uris = new ArrayList<>();
        final List<Uri> uris = new ArrayList<>();
        if (RemoteVolumePreferenceController.getActiveRemoteToken(mContext) != null) {

            uris.add(VOLUME_REMOTE_MEDIA_URI);
        uris.add(REMOTE_MEDIA_SLICE_URI);
        }
        uris.add(VOLUME_MEDIA_URI);
        uris.add(VOLUME_MEDIA_URI);
        uris.add(MEDIA_OUTPUT_INDICATOR_SLICE_URI);
        uris.add(MEDIA_OUTPUT_INDICATOR_SLICE_URI);
        uris.add(VOLUME_CALL_URI);
        uris.add(VOLUME_CALL_URI);
+12 −10
Original line number Original line Diff line number Diff line
@@ -41,6 +41,7 @@ import com.android.settings.homepage.contextualcards.slices.NotificationChannelS
import com.android.settings.location.LocationSlice;
import com.android.settings.location.LocationSlice;
import com.android.settings.media.MediaOutputIndicatorSlice;
import com.android.settings.media.MediaOutputIndicatorSlice;
import com.android.settings.media.MediaOutputSlice;
import com.android.settings.media.MediaOutputSlice;
import com.android.settings.media.RemoteMediaSlice;
import com.android.settings.network.telephony.MobileDataSlice;
import com.android.settings.network.telephony.MobileDataSlice;
import com.android.settings.notification.zen.ZenModeButtonPreferenceController;
import com.android.settings.notification.zen.ZenModeButtonPreferenceController;
import com.android.settings.wifi.calling.WifiCallingSliceHelper;
import com.android.settings.wifi.calling.WifiCallingSliceHelper;
@@ -224,16 +225,6 @@ public class CustomSliceRegistry {
            .appendPath("media_volume")
            .appendPath("media_volume")
            .build();
            .build();


    /**
     * Full {@link Uri} for the Remote Media Volume Slice.
     */
    public static final Uri VOLUME_REMOTE_MEDIA_URI = new Uri.Builder()
            .scheme(ContentResolver.SCHEME_CONTENT)
            .authority(SettingsSliceProvider.SLICE_AUTHORITY)
            .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
            .appendPath("remote_volume")
            .build();

    /**
    /**
     * Full {@link Uri} for the Ringer volume Slice.
     * Full {@link Uri} for the Ringer volume Slice.
     */
     */
@@ -312,6 +303,16 @@ public class CustomSliceRegistry {
            .appendPath("dark_theme")
            .appendPath("dark_theme")
            .build();
            .build();


    /**
     * Backing Uri for the Remote Media Slice.
     */
    public static Uri REMOTE_MEDIA_SLICE_URI = new Uri.Builder()
            .scheme(ContentResolver.SCHEME_CONTENT)
            .authority(SettingsSliceProvider.SLICE_AUTHORITY)
            .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
            .appendPath(MediaOutputSliceConstants.KEY_REMOTE_MEDIA)
            .build();

    @VisibleForTesting
    @VisibleForTesting
    static final Map<Uri, Class<? extends CustomSliceable>> sUriToSlice;
    static final Map<Uri, Class<? extends CustomSliceable>> sUriToSlice;


@@ -335,6 +336,7 @@ public class CustomSliceRegistry {
        sUriToSlice.put(STORAGE_SLICE_URI, StorageSlice.class);
        sUriToSlice.put(STORAGE_SLICE_URI, StorageSlice.class);
        sUriToSlice.put(WIFI_SLICE_URI, WifiSlice.class);
        sUriToSlice.put(WIFI_SLICE_URI, WifiSlice.class);
        sUriToSlice.put(DARK_THEME_SLICE_URI, DarkThemeSlice.class);
        sUriToSlice.put(DARK_THEME_SLICE_URI, DarkThemeSlice.class);
        sUriToSlice.put(REMOTE_MEDIA_SLICE_URI, RemoteMediaSlice.class);
    }
    }


    public static Class<? extends CustomSliceable> getSliceClassByUri(Uri uri) {
    public static Class<? extends CustomSliceable> getSliceClassByUri(Uri uri) {
+170 −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.slices.CustomSliceRegistry.REMOTE_MEDIA_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.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.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 = RemoteMediaSliceTest.ShadowSliceBackgroundWorker.class)
public class RemoteMediaSliceTest {

    private static final String MEDIA_ID = "media_id";
    private static final String TEST_PACKAGE_LABEL = "music";
    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 int TEST_VOLUME = 3;

    private static MediaDeviceUpdateWorker sMediaDeviceUpdateWorker;

    @Mock
    private LocalMediaManager mLocalMediaManager;
    @Mock
    private MediaDevice mDevice;

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

    private Context mContext;
    private RemoteMediaSlice mRemoteMediaSlice;

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

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

        mRemoteMediaSlice = new RemoteMediaSlice(mContext);
        sMediaDeviceUpdateWorker = spy(new MediaDeviceUpdateWorker(mContext,
                REMOTE_MEDIA_SLICE_URI));
        sMediaDeviceUpdateWorker.mLocalMediaManager = mLocalMediaManager;
        when(sMediaDeviceUpdateWorker.getActiveMediaDevice(
                MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE)).thenReturn(mDevices);
        when(mDevice.getId()).thenReturn(TEST_DEVICE_1_ID);
        when(mDevice.getName()).thenReturn(TEST_DEVICE_1_NAME);
        when(mDevice.getMaxVolume()).thenReturn(100);
        when(mDevice.getCurrentVolume()).thenReturn(10);
        when(mDevice.getClientAppLabel()).thenReturn(TEST_PACKAGE_LABEL);
    }

    @Test
    public void onNotifyChange_noId_doNothing() {
        mDevices.add(mDevice);
        when(mLocalMediaManager.getMediaDeviceById(mDevices, TEST_DEVICE_1_ID)).thenReturn(mDevice);
        sMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);
        final Intent intent = new Intent();
        intent.putExtra(EXTRA_RANGE_VALUE, TEST_VOLUME);

        mRemoteMediaSlice.onNotifyChange(intent);

        verify(mDevice, never()).requestSetVolume(anyInt());
    }

    @Test
    public void onNotifyChange_verifyAdjustVolume() {
        mDevices.add(mDevice);
        when(mLocalMediaManager.getMediaDeviceById(mDevices, TEST_DEVICE_1_ID)).thenReturn(mDevice);
        sMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);
        final Intent intent = new Intent();
        intent.putExtra(MEDIA_ID, TEST_DEVICE_1_ID);
        intent.putExtra(EXTRA_RANGE_VALUE, TEST_VOLUME);

        mRemoteMediaSlice.onNotifyChange(intent);

        verify(mDevice).requestSetVolume(TEST_VOLUME);
    }

    @Test
    public void getSlice_noActiveDevice_checkRowNumber() {
        final Slice slice = mRemoteMediaSlice.getSlice();
        final int rows = SliceQuery.findAll(slice, FORMAT_SLICE, HINT_LIST_ITEM, null).size();

        assertThat(rows).isEqualTo(0);
    }

    @Test
    public void getSlice_withActiveDevice_checkRowNumber() {
        mDevices.add(mDevice);
        final Slice slice = mRemoteMediaSlice.getSlice();
        final int rows = SliceQuery.findAll(slice, FORMAT_SLICE, HINT_LIST_ITEM, null).size();

        // InputRange and Row
        assertThat(rows).isEqualTo(2);
    }

    @Test
    public void getSlice_withActiveDevice_checkTitle() {
        mDevices.add(mDevice);
        final Slice slice = mRemoteMediaSlice.getSlice();
        final SliceMetadata metadata = SliceMetadata.from(mContext, slice);
        final SliceAction primaryAction = metadata.getPrimaryAction();

        assertThat(primaryAction.getTitle().toString()).isEqualTo(mContext.getText(
                com.android.settings.R.string.remote_media_volume_option_title)
                + " (" + TEST_PACKAGE_LABEL + ")");
    }

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

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