diff --git a/src/com/android/settings/media/MediaDeviceUpdateWorker.java b/src/com/android/settings/media/MediaDeviceUpdateWorker.java index 2d9a7367459..20f1150befe 100644 --- a/src/com/android/settings/media/MediaDeviceUpdateWorker.java +++ b/src/com/android/settings/media/MediaDeviceUpdateWorker.java @@ -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 getActiveMediaDevice(@MediaDevice.MediaDeviceType int type) { + return mLocalMediaManager.getActiveMediaDevice(type); + } + /** * Request to set volume. * diff --git a/src/com/android/settings/media/RemoteMediaSlice.java b/src/com/android/settings/media/RemoteMediaSlice.java new file mode 100644 index 00000000000..55b3e2c97a3 --- /dev/null +++ b/src/com/android/settings/media/RemoteMediaSlice.java @@ -0,0 +1,182 @@ +/* + * 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 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; + } +} diff --git a/src/com/android/settings/panel/VolumePanel.java b/src/com/android/settings/panel/VolumePanel.java index 1a166bae5c3..61dee1541cd 100644 --- a/src/com/android/settings/panel/VolumePanel.java +++ b/src/com/android/settings/panel/VolumePanel.java @@ -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 getSlices() { final List 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); diff --git a/src/com/android/settings/slices/CustomSliceRegistry.java b/src/com/android/settings/slices/CustomSliceRegistry.java index 700fe2ed0fd..13b09ce8a0b 100644 --- a/src/com/android/settings/slices/CustomSliceRegistry.java +++ b/src/com/android/settings/slices/CustomSliceRegistry.java @@ -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> 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 getSliceClassByUri(Uri uri) { diff --git a/tests/robotests/src/com/android/settings/media/RemoteMediaSliceTest.java b/tests/robotests/src/com/android/settings/media/RemoteMediaSliceTest.java new file mode 100644 index 00000000000..b719a9e8f93 --- /dev/null +++ b/tests/robotests/src/com/android/settings/media/RemoteMediaSliceTest.java @@ -0,0 +1,170 @@ +/* + * 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 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; + } + } +} diff --git a/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java b/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java index 8dd04ce82c5..4edc2c7c1e7 100644 --- a/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java +++ b/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java @@ -22,38 +22,22 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.content.Context; -import android.media.session.ISessionController; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.media.session.MediaSessionManager; import android.net.Uri; -import com.android.settings.notification.RemoteVolumePreferenceController; import com.android.settings.slices.CustomSliceRegistry; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import java.util.ArrayList; import java.util.List; @RunWith(RobolectricTestRunner.class) public class VolumePanelTest { - @Mock - private MediaSessionManager mMediaSessionManager; - @Mock - private MediaController mMediaController; - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private ISessionController mStub; - - private VolumePanel mPanel; private Context mContext; @@ -65,28 +49,16 @@ public class VolumePanelTest { mContext = spy(RuntimeEnvironment.application); when(mContext.getApplicationContext()).thenReturn(mContext); - when(mContext.getSystemService(MediaSessionManager.class)).thenReturn(mMediaSessionManager); mPanel = VolumePanel.create(mContext); } @Test - public void getSlices_hasActiveRemoteToken_containsRemoteMediaUri() { - List activeSessions = new ArrayList<>(); - MediaSession.Token token = new MediaSession.Token(mStub); - activeSessions.add(mMediaController); - - when(mMediaSessionManager.getActiveSessions(null)).thenReturn( - activeSessions); - when(mMediaController.getPlaybackInfo()).thenReturn(new MediaController.PlaybackInfo( - MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE, 0, 10, 5, null)); - when(mMediaController.getSessionToken()).thenReturn(new MediaSession.Token(mStub)); - when(RemoteVolumePreferenceController.getActiveRemoteToken(mContext)).thenReturn(token); - + public void getSlices_checkUri() { final List uris = mPanel.getSlices(); assertThat(uris).containsExactly( - CustomSliceRegistry.VOLUME_REMOTE_MEDIA_URI, + CustomSliceRegistry.REMOTE_MEDIA_SLICE_URI, CustomSliceRegistry.VOLUME_CALL_URI, CustomSliceRegistry.VOLUME_MEDIA_URI, CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI, @@ -94,22 +66,6 @@ public class VolumePanelTest { CustomSliceRegistry.VOLUME_ALARM_URI); } - @Test - public void getSlices_doesNotHaveActiveRemoteToken_doesNotcontainRemoteMediaUri() { - final List uris = mPanel.getSlices(); - - when(RemoteVolumePreferenceController.getActiveRemoteToken(mContext)) - .thenReturn(null); - - assertThat(uris).doesNotContain(CustomSliceRegistry.VOLUME_REMOTE_MEDIA_URI); - assertThat(uris).containsExactly( - CustomSliceRegistry.VOLUME_CALL_URI, - CustomSliceRegistry.VOLUME_MEDIA_URI, - CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI, - CustomSliceRegistry.VOLUME_RINGER_URI, - CustomSliceRegistry.VOLUME_ALARM_URI); - } - @Test public void getSeeMoreIntent_notNull() { assertThat(mPanel.getSeeMoreIntent()).isNotNull();