diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 6724ff5355d..083b34916a6 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -100,6 +100,7 @@ + Media volume + + Cast volume + Call volume diff --git a/res/xml/sound_settings.xml b/res/xml/sound_settings.xml index ee8613d5fca..81a04538430 100644 --- a/res/xml/sound_settings.xml +++ b/res/xml/sound_settings.xml @@ -22,6 +22,15 @@ settings:keywords="@string/keywords_sounds" settings:initialExpandedChildrenCount="9"> + + + controllers = mMediaSessionManager.getActiveSessions(null); + for (MediaController mediaController : controllers) { + final MediaController.PlaybackInfo pi = mediaController.getPlaybackInfo(); + if (isRemote(pi)) { + mActiveToken = mediaController.getSessionToken(); + return AVAILABLE; + } + } + + // No active remote media at this point + return CONDITIONALLY_UNAVAILABLE; + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void onResume() { + super.onResume(); + mMediaSessions.init(); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + public void onPause() { + super.onPause(); + mMediaSessions.destroy(); + } + + @Override + public int getSliderPosition() { + if (mPreference != null) { + return mPreference.getProgress(); + } + //TODO(b/126199571): get it from media controller + return 0; + } + + @Override + public boolean setSliderPosition(int position) { + if (mPreference != null) { + mPreference.setProgress(position); + } + //TODO(b/126199571): set it through media controller + return false; + } + + @Override + public int getMaxSteps() { + if (mPreference != null) { + return mPreference.getMax(); + } + //TODO(b/126199571): get it from media controller + return 0; + } + + @Override + public boolean isSliceable() { + return TextUtils.equals(getPreferenceKey(), KEY_REMOTE_VOLUME); + } + + @Override + public String getPreferenceKey() { + return KEY_REMOTE_VOLUME; + } + + @Override + public int getAudioStream() { + // This can be anything because remote volume controller doesn't rely on it. + return REMOTE_VOLUME; + } + + @Override + public int getMuteIcon() { + return R.drawable.ic_volume_remote_mute; + } + + public static boolean isRemote(MediaController.PlaybackInfo pi) { + return pi != null + && pi.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE; + } +} diff --git a/src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java b/src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java new file mode 100644 index 00000000000..e99af6a42cb --- /dev/null +++ b/src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2019 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.notification; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * A slider preference that controls remote volume, which doesn't go through + * {@link android.media.AudioManager} + **/ +public class RemoteVolumeSeekBarPreference extends VolumeSeekBarPreference { + + public RemoteVolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public RemoteVolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public RemoteVolumeSeekBarPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RemoteVolumeSeekBarPreference(Context context) { + super(context); + } + + @Override + public void setStream(int stream) { + // Do nothing here, volume is not controlled by AudioManager + } + + @Override + protected void init() { + if (mSeekBar == null) return; + updateIconView(); + updateSuppressionText(); + } +} diff --git a/src/com/android/settings/notification/SoundSettings.java b/src/com/android/settings/notification/SoundSettings.java index e8b7ee0d782..b26b9219cd4 100644 --- a/src/com/android/settings/notification/SoundSettings.java +++ b/src/com/android/settings/notification/SoundSettings.java @@ -184,6 +184,7 @@ public class SoundSettings extends DashboardFragment implements OnActivityResult volumeControllers.add(use(RingVolumePreferenceController.class)); volumeControllers.add(use(NotificationVolumePreferenceController.class)); volumeControllers.add(use(CallVolumePreferenceController.class)); + volumeControllers.add(use(RemoteVolumePreferenceController.class)); use(MediaOutputPreferenceController.class).setCallback(listPreference -> onPreferenceDataChanged(listPreference)); diff --git a/src/com/android/settings/notification/VolumeSeekBarPreference.java b/src/com/android/settings/notification/VolumeSeekBarPreference.java index 13f630004d1..7f36791e069 100644 --- a/src/com/android/settings/notification/VolumeSeekBarPreference.java +++ b/src/com/android/settings/notification/VolumeSeekBarPreference.java @@ -40,8 +40,8 @@ import java.util.Objects; public class VolumeSeekBarPreference extends SeekBarPreference { private static final String TAG = "VolumeSeekBarPreference"; + protected SeekBar mSeekBar; private int mStream; - private SeekBar mSeekBar; private SeekBarVolumizer mVolumizer; private Callback mCallback; private ImageView mIconView; @@ -121,7 +121,7 @@ public class VolumeSeekBarPreference extends SeekBarPreference { init(); } - private void init() { + protected void init() { if (mSeekBar == null) return; final SeekBarVolumizer.Callback sbvc = new SeekBarVolumizer.Callback() { @Override @@ -158,7 +158,7 @@ public class VolumeSeekBarPreference extends SeekBarPreference { } } - private void updateIconView() { + protected void updateIconView() { if (mIconView == null) return; if (mIconResId != 0) { mIconView.setImageResource(mIconResId); @@ -195,7 +195,7 @@ public class VolumeSeekBarPreference extends SeekBarPreference { updateSuppressionText(); } - private void updateSuppressionText() { + protected void updateSuppressionText() { if (mSuppressionTextView != null && mSeekBar != null) { mSuppressionTextView.setText(mSuppressionText); final boolean showSuppression = !TextUtils.isEmpty(mSuppressionText); diff --git a/tests/robotests/src/com/android/settings/notification/RemoteVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/RemoteVolumePreferenceControllerTest.java new file mode 100644 index 00000000000..a34d4d7a37c --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/RemoteVolumePreferenceControllerTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2019 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.notification; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; + +import com.android.settings.R; + +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 java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class RemoteVolumePreferenceControllerTest { + + @Mock + private MediaSessionManager mMediaSessionManager; + @Mock + private MediaController mMediaController; + private RemoteVolumePreferenceController mController; + private Context mContext; + private List mActiveSessions; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + when(mContext.getSystemService(MediaSessionManager.class)).thenReturn(mMediaSessionManager); + mActiveSessions = new ArrayList<>(); + mActiveSessions.add(mMediaController); + when(mMediaSessionManager.getActiveSessions(null)).thenReturn( + mActiveSessions); + + mController = new RemoteVolumePreferenceController(mContext); + } + + @Test + public void isAvailable_containRemoteMedia_returnTrue() { + when(mMediaController.getPlaybackInfo()).thenReturn( + new MediaController.PlaybackInfo(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE, + 0, 0, 0, null)); + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_noRemoteMedia_returnFalse() { + when(mMediaController.getPlaybackInfo()).thenReturn( + new MediaController.PlaybackInfo(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, + 0, 0, 0, null)); + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void getMuteIcon_returnMuteIcon() { + assertThat(mController.getMuteIcon()).isEqualTo(R.drawable.ic_volume_remote_mute); + } + + @Test + public void getAudioStream_returnRemoteVolume() { + assertThat(mController.getAudioStream()).isEqualTo( + RemoteVolumePreferenceController.REMOTE_VOLUME); + } +}