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

Commit ff6e3415 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add remote media slice in volume panel"

parents 910cc87f b266fa60
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -146,6 +146,17 @@ public class MediaDeviceUpdateWorker extends SliceBackgroundWorker
        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.
     *
+182 −0
Original line number 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 Diff line number Diff line
@@ -17,10 +17,10 @@
package com.android.settings.panel;

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_CALL_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 android.app.settings.SettingsEnums;
@@ -30,7 +30,6 @@ import android.net.Uri;
import android.provider.Settings;

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

import java.util.ArrayList;
import java.util.List;
@@ -55,9 +54,8 @@ public class VolumePanel implements PanelContent {
    @Override
    public List<Uri> getSlices() {
        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(MEDIA_OUTPUT_INDICATOR_SLICE_URI);
        uris.add(VOLUME_CALL_URI);
+12 −10
Original line number 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.media.MediaOutputIndicatorSlice;
import com.android.settings.media.MediaOutputSlice;
import com.android.settings.media.RemoteMediaSlice;
import com.android.settings.network.telephony.MobileDataSlice;
import com.android.settings.notification.zen.ZenModeButtonPreferenceController;
import com.android.settings.wifi.calling.WifiCallingSliceHelper;
@@ -224,16 +225,6 @@ public class CustomSliceRegistry {
            .appendPath("media_volume")
            .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.
     */
@@ -312,6 +303,16 @@ public class CustomSliceRegistry {
            .appendPath("dark_theme")
            .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
    static final Map<Uri, Class<? extends CustomSliceable>> sUriToSlice;

@@ -335,6 +336,7 @@ public class CustomSliceRegistry {
        sUriToSlice.put(STORAGE_SLICE_URI, StorageSlice.class);
        sUriToSlice.put(WIFI_SLICE_URI, WifiSlice.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) {
+170 −0
Original line number 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