1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.notification; 18 19 import android.content.Context; 20 import android.media.session.MediaController; 21 import android.media.session.MediaSession; 22 import android.media.session.MediaSessionManager; 23 import android.net.Uri; 24 import android.os.Looper; 25 import android.text.TextUtils; 26 27 import androidx.annotation.VisibleForTesting; 28 import androidx.lifecycle.OnLifecycleEvent; 29 import androidx.preference.PreferenceScreen; 30 31 import com.android.settings.R; 32 import com.android.settings.slices.SliceBackgroundWorker; 33 import com.android.settingslib.core.lifecycle.Lifecycle; 34 import com.android.settingslib.volume.MediaSessions; 35 36 import java.io.IOException; 37 import java.util.List; 38 import java.util.Objects; 39 40 public class RemoteVolumePreferenceController extends VolumeSeekBarPreferenceController { 41 42 private static final String KEY_REMOTE_VOLUME = "remote_volume"; 43 @VisibleForTesting 44 static final int REMOTE_VOLUME = 100; 45 46 private MediaSessions mMediaSessions; 47 @VisibleForTesting 48 MediaSession.Token mActiveToken; 49 @VisibleForTesting 50 MediaController mMediaController; 51 52 @VisibleForTesting 53 MediaSessions.Callbacks mCallbacks = new MediaSessions.Callbacks() { 54 @Override 55 public void onRemoteUpdate(MediaSession.Token token, String name, 56 MediaController.PlaybackInfo pi) { 57 if (mActiveToken == null) { 58 updateToken(token); 59 } 60 if (Objects.equals(mActiveToken, token)) { 61 updatePreference(mPreference, mActiveToken, pi); 62 } 63 } 64 65 @Override 66 public void onRemoteRemoved(MediaSession.Token t) { 67 if (Objects.equals(mActiveToken, t)) { 68 updateToken(null); 69 if (mPreference != null) { 70 mPreference.setVisible(false); 71 } 72 } 73 } 74 75 @Override 76 public void onRemoteVolumeChanged(MediaSession.Token token, int flags) { 77 if (Objects.equals(mActiveToken, token)) { 78 final MediaController.PlaybackInfo pi = mMediaController.getPlaybackInfo(); 79 if (pi != null) { 80 setSliderPosition(pi.getCurrentVolume()); 81 } 82 } 83 } 84 }; 85 RemoteVolumePreferenceController(Context context)86 public RemoteVolumePreferenceController(Context context) { 87 super(context, KEY_REMOTE_VOLUME); 88 mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), mCallbacks); 89 updateToken(getActiveRemoteToken(mContext)); 90 } 91 92 @Override getAvailabilityStatus()93 public int getAvailabilityStatus() { 94 // Always return true to make it indexed in database 95 return AVAILABLE_UNSEARCHABLE; 96 } 97 98 /** 99 * Return {@link android.media.session.MediaSession.Token} for active remote token, or 100 * {@code null} if there is no active remote token. 101 */ getActiveRemoteToken(Context context)102 public static MediaSession.Token getActiveRemoteToken(Context context) { 103 final MediaSessionManager sessionManager = context.getSystemService( 104 MediaSessionManager.class); 105 final List<MediaController> controllers = sessionManager.getActiveSessions(null); 106 for (MediaController mediaController : controllers) { 107 final MediaController.PlaybackInfo pi = mediaController.getPlaybackInfo(); 108 if (isRemote(pi)) { 109 return mediaController.getSessionToken(); 110 } 111 } 112 113 // No active remote media at this point 114 return null; 115 } 116 117 @Override displayPreference(PreferenceScreen screen)118 public void displayPreference(PreferenceScreen screen) { 119 super.displayPreference(screen); 120 mPreference.setVisible(mActiveToken != null); 121 if (mMediaController != null) { 122 updatePreference(mPreference, mActiveToken, mMediaController.getPlaybackInfo()); 123 } 124 } 125 126 @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) onResume()127 public void onResume() { 128 super.onResume(); 129 mMediaSessions.init(); 130 } 131 132 @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) onPause()133 public void onPause() { 134 super.onPause(); 135 mMediaSessions.destroy(); 136 } 137 138 @Override getSliderPosition()139 public int getSliderPosition() { 140 if (mPreference != null) { 141 return mPreference.getProgress(); 142 } 143 if (mMediaController == null) { 144 return 0; 145 } 146 final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); 147 return playbackInfo != null ? playbackInfo.getCurrentVolume() : 0; 148 } 149 150 @Override setSliderPosition(int position)151 public boolean setSliderPosition(int position) { 152 if (mPreference != null) { 153 mPreference.setProgress(position); 154 } 155 if (mMediaController == null) { 156 return false; 157 } 158 mMediaController.setVolumeTo(position, 0); 159 return true; 160 } 161 162 @Override getMax()163 public int getMax() { 164 if (mPreference != null) { 165 return mPreference.getMax(); 166 } 167 if (mMediaController == null) { 168 return 0; 169 } 170 final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); 171 return playbackInfo != null ? playbackInfo.getMaxVolume() : 0; 172 } 173 174 @Override getMin()175 public int getMin() { 176 if (mPreference != null) { 177 return mPreference.getMin(); 178 } 179 return 0; 180 } 181 182 @Override isSliceable()183 public boolean isSliceable() { 184 return TextUtils.equals(getPreferenceKey(), KEY_REMOTE_VOLUME); 185 } 186 187 @Override useDynamicSliceSummary()188 public boolean useDynamicSliceSummary() { 189 return true; 190 } 191 192 @Override getPreferenceKey()193 public String getPreferenceKey() { 194 return KEY_REMOTE_VOLUME; 195 } 196 197 @Override getAudioStream()198 public int getAudioStream() { 199 // This can be anything because remote volume controller doesn't rely on it. 200 return REMOTE_VOLUME; 201 } 202 203 @Override getMuteIcon()204 public int getMuteIcon() { 205 return R.drawable.ic_volume_remote_mute; 206 } 207 isRemote(MediaController.PlaybackInfo pi)208 public static boolean isRemote(MediaController.PlaybackInfo pi) { 209 return pi != null 210 && pi.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE; 211 } 212 213 @Override getBackgroundWorkerClass()214 public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() { 215 return RemoteVolumeSliceWorker.class; 216 } 217 updatePreference(VolumeSeekBarPreference seekBarPreference, MediaSession.Token token, MediaController.PlaybackInfo playbackInfo)218 private void updatePreference(VolumeSeekBarPreference seekBarPreference, 219 MediaSession.Token token, MediaController.PlaybackInfo playbackInfo) { 220 if (seekBarPreference == null || token == null || playbackInfo == null) { 221 return; 222 } 223 224 seekBarPreference.setMax(playbackInfo.getMaxVolume()); 225 seekBarPreference.setVisible(true); 226 setSliderPosition(playbackInfo.getCurrentVolume()); 227 } 228 updateToken(MediaSession.Token token)229 private void updateToken(MediaSession.Token token) { 230 mActiveToken = token; 231 if (token != null) { 232 mMediaController = new MediaController(mContext, mActiveToken); 233 } else { 234 mMediaController = null; 235 } 236 } 237 238 /** 239 * Listener for background change to remote volume, which listens callback 240 * from {@code MediaSessions} 241 */ 242 public static class RemoteVolumeSliceWorker extends SliceBackgroundWorker<Void> implements 243 MediaSessions.Callbacks { 244 245 private MediaSessions mMediaSessions; 246 RemoteVolumeSliceWorker(Context context, Uri uri)247 public RemoteVolumeSliceWorker(Context context, Uri uri) { 248 super(context, uri); 249 mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), this); 250 } 251 252 @Override onSlicePinned()253 protected void onSlicePinned() { 254 mMediaSessions.init(); 255 } 256 257 @Override onSliceUnpinned()258 protected void onSliceUnpinned() { 259 mMediaSessions.destroy(); 260 } 261 262 @Override close()263 public void close() throws IOException { 264 mMediaSessions = null; 265 } 266 267 @Override onRemoteUpdate(MediaSession.Token token, String name, MediaController.PlaybackInfo pi)268 public void onRemoteUpdate(MediaSession.Token token, String name, 269 MediaController.PlaybackInfo pi) { 270 notifySliceChange(); 271 } 272 273 @Override onRemoteRemoved(MediaSession.Token t)274 public void onRemoteRemoved(MediaSession.Token t) { 275 notifySliceChange(); 276 } 277 278 @Override onRemoteVolumeChanged(MediaSession.Token token, int flags)279 public void onRemoteVolumeChanged(MediaSession.Token token, int flags) { 280 notifySliceChange(); 281 } 282 } 283 } 284