Loading AndroidManifest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -100,6 +100,7 @@ <uses-permission android:name="android.permission.USE_RESERVED_DISK" /> <uses-permission android:name="android.permission.MANAGE_SCOPED_ACCESS_DIRECTORY_PERMISSIONS" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" /> <application android:label="@string/settings_label" android:icon="@drawable/ic_launcher_settings" Loading res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -7193,6 +7193,9 @@ <!-- Sound: Title for the option managing media volume. [CHAR LIMIT=30] --> <string name="media_volume_option_title">Media volume</string> <!-- Sound: Title for the option managing remote media volume. [CHAR LIMIT=30] --> <string name="remote_media_volume_option_title">Cast volume</string> <!-- Sound: Title for the option managing call volume. [CHAR LIMIT=30] --> <string name="call_volume_option_title">Call volume</string> res/xml/sound_settings.xml +9 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,15 @@ settings:keywords="@string/keywords_sounds" settings:initialExpandedChildrenCount="9"> <!-- Remote volume --> <com.android.settings.notification.RemoteVolumeSeekBarPreference android:key="remote_volume" android:icon="@drawable/ic_volume_remote" android:title="@string/remote_media_volume_option_title" android:order="-185" settings:allowDynamicSummaryInSlice="true" settings:controller="com.android.settings.notification.RemoteVolumePreferenceController"/> <!-- Media volume --> <com.android.settings.notification.VolumeSeekBarPreference android:key="media_volume" Loading src/com/android/settings/notification/RemoteVolumePreferenceController.java 0 → 100644 +261 −0 Original line number Diff line number Diff line /* * 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.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.net.Uri; import android.os.Looper; import android.text.TextUtils; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.OnLifecycleEvent; import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.slices.SliceBackgroundWorker; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.volume.MediaSessions; import java.io.IOException; import java.util.List; public class RemoteVolumePreferenceController extends VolumeSeekBarPreferenceController { private static final String KEY_REMOTE_VOLUME = "remote_volume"; @VisibleForTesting static final int REMOTE_VOLUME = 100; private MediaSessionManager mMediaSessionManager; private MediaSessions mMediaSessions; @VisibleForTesting MediaSession.Token mActiveToken; @VisibleForTesting MediaController mMediaController; @VisibleForTesting MediaSessions.Callbacks mCallbacks = new MediaSessions.Callbacks() { @Override public void onRemoteUpdate(MediaSession.Token token, String name, MediaController.PlaybackInfo pi) { if (mActiveToken == null) { updateToken(token); } if (mActiveToken == token) { updatePreference(mPreference, mActiveToken, pi); } } @Override public void onRemoteRemoved(MediaSession.Token t) { if (mActiveToken == t) { updateToken(null); if (mPreference != null) { mPreference.setVisible(false); } } } @Override public void onRemoteVolumeChanged(MediaSession.Token token, int flags) { if (mActiveToken == token) { final MediaController.PlaybackInfo pi = mMediaController.getPlaybackInfo(); if (pi != null) { setSliderPosition(pi.getCurrentVolume()); } } } }; public RemoteVolumePreferenceController(Context context) { super(context, KEY_REMOTE_VOLUME); mMediaSessionManager = context.getSystemService(MediaSessionManager.class); mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), mCallbacks); } @Override public int getAvailabilityStatus() { final List<MediaController> controllers = mMediaSessionManager.getActiveSessions(null); for (MediaController mediaController : controllers) { final MediaController.PlaybackInfo pi = mediaController.getPlaybackInfo(); if (isRemote(pi)) { updateToken(mediaController.getSessionToken()); return AVAILABLE; } } // No active remote media at this point return CONDITIONALLY_UNAVAILABLE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (mMediaController != null) { updatePreference(mPreference, mActiveToken, mMediaController.getPlaybackInfo()); } } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void onResume() { super.onResume(); //TODO(b/126199571): register callback once b/126890783 is fixed } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void onPause() { super.onPause(); //TODO(b/126199571): unregister callback once b/126890783 is fixed } @Override public int getSliderPosition() { if (mPreference != null) { return mPreference.getProgress(); } if (mMediaController == null) { return 0; } final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); return playbackInfo != null ? playbackInfo.getCurrentVolume() : 0; } @Override public boolean setSliderPosition(int position) { if (mPreference != null) { mPreference.setProgress(position); } if (mMediaController == null) { return false; } mMediaController.setVolumeTo(position, 0); return true; } @Override public int getMaxSteps() { if (mPreference != null) { return mPreference.getMax(); } if (mMediaController == null) { return 0; } final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); return playbackInfo != null ? playbackInfo.getMaxVolume() : 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; } @Override public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() { //TODO(b/126199571): return RemoteVolumeSliceWorker once b/126890783 is fixed return null; } private void updatePreference(VolumeSeekBarPreference seekBarPreference, MediaSession.Token token, MediaController.PlaybackInfo playbackInfo) { if (seekBarPreference == null || token == null || playbackInfo == null) { return; } seekBarPreference.setMax(playbackInfo.getMaxVolume()); seekBarPreference.setVisible(true); setSliderPosition(playbackInfo.getCurrentVolume()); } private void updateToken(MediaSession.Token token) { mActiveToken = token; if (token != null) { mMediaController = new MediaController(mContext, mActiveToken); } else { mMediaController = null; } } /** * Listener for background change to remote volume, which listens callback * from {@code MediaSessions} */ public static class RemoteVolumeSliceWorker extends SliceBackgroundWorker<Void> implements MediaSessions.Callbacks { private MediaSessions mMediaSessions; public RemoteVolumeSliceWorker(Context context, Uri uri) { super(context, uri); mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), this); } @Override protected void onSlicePinned() { mMediaSessions.init(); } @Override protected void onSliceUnpinned() { mMediaSessions.destroy(); } @Override public void close() throws IOException { mMediaSessions = null; } @Override public void onRemoteUpdate(MediaSession.Token token, String name, MediaController.PlaybackInfo pi) { notifySliceChange(); } @Override public void onRemoteRemoved(MediaSession.Token t) { notifySliceChange(); } @Override public void onRemoteVolumeChanged(MediaSession.Token token, int flags) { notifySliceChange(); } } } src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java 0 → 100644 +56 −0 Original line number Diff line number Diff line /* * 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(); } } Loading
AndroidManifest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -100,6 +100,7 @@ <uses-permission android:name="android.permission.USE_RESERVED_DISK" /> <uses-permission android:name="android.permission.MANAGE_SCOPED_ACCESS_DIRECTORY_PERMISSIONS" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" /> <application android:label="@string/settings_label" android:icon="@drawable/ic_launcher_settings" Loading
res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -7193,6 +7193,9 @@ <!-- Sound: Title for the option managing media volume. [CHAR LIMIT=30] --> <string name="media_volume_option_title">Media volume</string> <!-- Sound: Title for the option managing remote media volume. [CHAR LIMIT=30] --> <string name="remote_media_volume_option_title">Cast volume</string> <!-- Sound: Title for the option managing call volume. [CHAR LIMIT=30] --> <string name="call_volume_option_title">Call volume</string>
res/xml/sound_settings.xml +9 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,15 @@ settings:keywords="@string/keywords_sounds" settings:initialExpandedChildrenCount="9"> <!-- Remote volume --> <com.android.settings.notification.RemoteVolumeSeekBarPreference android:key="remote_volume" android:icon="@drawable/ic_volume_remote" android:title="@string/remote_media_volume_option_title" android:order="-185" settings:allowDynamicSummaryInSlice="true" settings:controller="com.android.settings.notification.RemoteVolumePreferenceController"/> <!-- Media volume --> <com.android.settings.notification.VolumeSeekBarPreference android:key="media_volume" Loading
src/com/android/settings/notification/RemoteVolumePreferenceController.java 0 → 100644 +261 −0 Original line number Diff line number Diff line /* * 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.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.net.Uri; import android.os.Looper; import android.text.TextUtils; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.OnLifecycleEvent; import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.slices.SliceBackgroundWorker; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.volume.MediaSessions; import java.io.IOException; import java.util.List; public class RemoteVolumePreferenceController extends VolumeSeekBarPreferenceController { private static final String KEY_REMOTE_VOLUME = "remote_volume"; @VisibleForTesting static final int REMOTE_VOLUME = 100; private MediaSessionManager mMediaSessionManager; private MediaSessions mMediaSessions; @VisibleForTesting MediaSession.Token mActiveToken; @VisibleForTesting MediaController mMediaController; @VisibleForTesting MediaSessions.Callbacks mCallbacks = new MediaSessions.Callbacks() { @Override public void onRemoteUpdate(MediaSession.Token token, String name, MediaController.PlaybackInfo pi) { if (mActiveToken == null) { updateToken(token); } if (mActiveToken == token) { updatePreference(mPreference, mActiveToken, pi); } } @Override public void onRemoteRemoved(MediaSession.Token t) { if (mActiveToken == t) { updateToken(null); if (mPreference != null) { mPreference.setVisible(false); } } } @Override public void onRemoteVolumeChanged(MediaSession.Token token, int flags) { if (mActiveToken == token) { final MediaController.PlaybackInfo pi = mMediaController.getPlaybackInfo(); if (pi != null) { setSliderPosition(pi.getCurrentVolume()); } } } }; public RemoteVolumePreferenceController(Context context) { super(context, KEY_REMOTE_VOLUME); mMediaSessionManager = context.getSystemService(MediaSessionManager.class); mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), mCallbacks); } @Override public int getAvailabilityStatus() { final List<MediaController> controllers = mMediaSessionManager.getActiveSessions(null); for (MediaController mediaController : controllers) { final MediaController.PlaybackInfo pi = mediaController.getPlaybackInfo(); if (isRemote(pi)) { updateToken(mediaController.getSessionToken()); return AVAILABLE; } } // No active remote media at this point return CONDITIONALLY_UNAVAILABLE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (mMediaController != null) { updatePreference(mPreference, mActiveToken, mMediaController.getPlaybackInfo()); } } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void onResume() { super.onResume(); //TODO(b/126199571): register callback once b/126890783 is fixed } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void onPause() { super.onPause(); //TODO(b/126199571): unregister callback once b/126890783 is fixed } @Override public int getSliderPosition() { if (mPreference != null) { return mPreference.getProgress(); } if (mMediaController == null) { return 0; } final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); return playbackInfo != null ? playbackInfo.getCurrentVolume() : 0; } @Override public boolean setSliderPosition(int position) { if (mPreference != null) { mPreference.setProgress(position); } if (mMediaController == null) { return false; } mMediaController.setVolumeTo(position, 0); return true; } @Override public int getMaxSteps() { if (mPreference != null) { return mPreference.getMax(); } if (mMediaController == null) { return 0; } final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); return playbackInfo != null ? playbackInfo.getMaxVolume() : 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; } @Override public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() { //TODO(b/126199571): return RemoteVolumeSliceWorker once b/126890783 is fixed return null; } private void updatePreference(VolumeSeekBarPreference seekBarPreference, MediaSession.Token token, MediaController.PlaybackInfo playbackInfo) { if (seekBarPreference == null || token == null || playbackInfo == null) { return; } seekBarPreference.setMax(playbackInfo.getMaxVolume()); seekBarPreference.setVisible(true); setSliderPosition(playbackInfo.getCurrentVolume()); } private void updateToken(MediaSession.Token token) { mActiveToken = token; if (token != null) { mMediaController = new MediaController(mContext, mActiveToken); } else { mMediaController = null; } } /** * Listener for background change to remote volume, which listens callback * from {@code MediaSessions} */ public static class RemoteVolumeSliceWorker extends SliceBackgroundWorker<Void> implements MediaSessions.Callbacks { private MediaSessions mMediaSessions; public RemoteVolumeSliceWorker(Context context, Uri uri) { super(context, uri); mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), this); } @Override protected void onSlicePinned() { mMediaSessions.init(); } @Override protected void onSliceUnpinned() { mMediaSessions.destroy(); } @Override public void close() throws IOException { mMediaSessions = null; } @Override public void onRemoteUpdate(MediaSession.Token token, String name, MediaController.PlaybackInfo pi) { notifySliceChange(); } @Override public void onRemoteRemoved(MediaSession.Token t) { notifySliceChange(); } @Override public void onRemoteVolumeChanged(MediaSession.Token token, int flags) { notifySliceChange(); } } }
src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java 0 → 100644 +56 −0 Original line number Diff line number Diff line /* * 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(); } }