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.car.developeroptions.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.car.developeroptions.R; 32 import com.android.car.developeroptions.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 MediaSessionManager mMediaSessionManager; 47 private MediaSessions mMediaSessions; 48 @VisibleForTesting 49 MediaSession.Token mActiveToken; 50 @VisibleForTesting 51 MediaController mMediaController; 52 53 @VisibleForTesting 54 MediaSessions.Callbacks mCallbacks = new MediaSessions.Callbacks() { 55 @Override 56 public void onRemoteUpdate(MediaSession.Token token, String name, 57 MediaController.PlaybackInfo pi) { 58 if (mActiveToken == null) { 59 updateToken(token); 60 } 61 if (Objects.equals(mActiveToken, token)) { 62 updatePreference(mPreference, mActiveToken, pi); 63 } 64 } 65 66 @Override 67 public void onRemoteRemoved(MediaSession.Token t) { 68 if (Objects.equals(mActiveToken, t)) { 69 updateToken(null); 70 if (mPreference != null) { 71 mPreference.setVisible(false); 72 } 73 } 74 } 75 76 @Override 77 public void onRemoteVolumeChanged(MediaSession.Token token, int flags) { 78 if (Objects.equals(mActiveToken, token)) { 79 final MediaController.PlaybackInfo pi = mMediaController.getPlaybackInfo(); 80 if (pi != null) { 81 setSliderPosition(pi.getCurrentVolume()); 82 } 83 } 84 } 85 }; 86 RemoteVolumePreferenceController(Context context)87 public RemoteVolumePreferenceController(Context context) { 88 super(context, KEY_REMOTE_VOLUME); 89 mMediaSessionManager = context.getSystemService(MediaSessionManager.class); 90 mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), mCallbacks); 91 } 92 93 @Override getAvailabilityStatus()94 public int getAvailabilityStatus() { 95 final List<MediaController> controllers = mMediaSessionManager.getActiveSessions(null); 96 for (MediaController mediaController : controllers) { 97 final MediaController.PlaybackInfo pi = mediaController.getPlaybackInfo(); 98 if (isRemote(pi)) { 99 updateToken(mediaController.getSessionToken()); 100 return AVAILABLE; 101 } 102 } 103 104 // No active remote media at this point 105 return CONDITIONALLY_UNAVAILABLE; 106 } 107 108 @Override displayPreference(PreferenceScreen screen)109 public void displayPreference(PreferenceScreen screen) { 110 super.displayPreference(screen); 111 if (mMediaController != null) { 112 updatePreference(mPreference, mActiveToken, mMediaController.getPlaybackInfo()); 113 } 114 } 115 116 @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) onResume()117 public void onResume() { 118 super.onResume(); 119 mMediaSessions.init(); 120 } 121 122 @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) onPause()123 public void onPause() { 124 super.onPause(); 125 mMediaSessions.destroy(); 126 } 127 128 @Override getSliderPosition()129 public int getSliderPosition() { 130 if (mPreference != null) { 131 return mPreference.getProgress(); 132 } 133 if (mMediaController == null) { 134 return 0; 135 } 136 final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); 137 return playbackInfo != null ? playbackInfo.getCurrentVolume() : 0; 138 } 139 140 @Override setSliderPosition(int position)141 public boolean setSliderPosition(int position) { 142 if (mPreference != null) { 143 mPreference.setProgress(position); 144 } 145 if (mMediaController == null) { 146 return false; 147 } 148 mMediaController.setVolumeTo(position, 0); 149 return true; 150 } 151 152 @Override getMaxSteps()153 public int getMaxSteps() { 154 if (mPreference != null) { 155 return mPreference.getMax(); 156 } 157 if (mMediaController == null) { 158 return 0; 159 } 160 final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); 161 return playbackInfo != null ? playbackInfo.getMaxVolume() : 0; 162 } 163 164 @Override isSliceable()165 public boolean isSliceable() { 166 return TextUtils.equals(getPreferenceKey(), KEY_REMOTE_VOLUME); 167 } 168 169 @Override useDynamicSliceSummary()170 public boolean useDynamicSliceSummary() { 171 return true; 172 } 173 174 @Override getPreferenceKey()175 public String getPreferenceKey() { 176 return KEY_REMOTE_VOLUME; 177 } 178 179 @Override getAudioStream()180 public int getAudioStream() { 181 // This can be anything because remote volume controller doesn't rely on it. 182 return REMOTE_VOLUME; 183 } 184 185 @Override getMuteIcon()186 public int getMuteIcon() { 187 return R.drawable.ic_volume_remote_mute; 188 } 189 isRemote(MediaController.PlaybackInfo pi)190 public static boolean isRemote(MediaController.PlaybackInfo pi) { 191 return pi != null 192 && pi.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE; 193 } 194 195 @Override getBackgroundWorkerClass()196 public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() { 197 return RemoteVolumeSliceWorker.class; 198 } 199 updatePreference(VolumeSeekBarPreference seekBarPreference, MediaSession.Token token, MediaController.PlaybackInfo playbackInfo)200 private void updatePreference(VolumeSeekBarPreference seekBarPreference, 201 MediaSession.Token token, MediaController.PlaybackInfo playbackInfo) { 202 if (seekBarPreference == null || token == null || playbackInfo == null) { 203 return; 204 } 205 206 seekBarPreference.setMax(playbackInfo.getMaxVolume()); 207 seekBarPreference.setVisible(true); 208 setSliderPosition(playbackInfo.getCurrentVolume()); 209 } 210 updateToken(MediaSession.Token token)211 private void updateToken(MediaSession.Token token) { 212 mActiveToken = token; 213 if (token != null) { 214 mMediaController = new MediaController(mContext, mActiveToken); 215 } else { 216 mMediaController = null; 217 } 218 } 219 220 /** 221 * Listener for background change to remote volume, which listens callback 222 * from {@code MediaSessions} 223 */ 224 public static class RemoteVolumeSliceWorker extends SliceBackgroundWorker<Void> implements 225 MediaSessions.Callbacks { 226 227 private MediaSessions mMediaSessions; 228 RemoteVolumeSliceWorker(Context context, Uri uri)229 public RemoteVolumeSliceWorker(Context context, Uri uri) { 230 super(context, uri); 231 mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), this); 232 } 233 234 @Override onSlicePinned()235 protected void onSlicePinned() { 236 mMediaSessions.init(); 237 } 238 239 @Override onSliceUnpinned()240 protected void onSliceUnpinned() { 241 mMediaSessions.destroy(); 242 } 243 244 @Override close()245 public void close() throws IOException { 246 mMediaSessions = null; 247 } 248 249 @Override onRemoteUpdate(MediaSession.Token token, String name, MediaController.PlaybackInfo pi)250 public void onRemoteUpdate(MediaSession.Token token, String name, 251 MediaController.PlaybackInfo pi) { 252 notifySliceChange(); 253 } 254 255 @Override onRemoteRemoved(MediaSession.Token t)256 public void onRemoteRemoved(MediaSession.Token t) { 257 notifySliceChange(); 258 } 259 260 @Override onRemoteVolumeChanged(MediaSession.Token token, int flags)261 public void onRemoteVolumeChanged(MediaSession.Token token, int flags) { 262 notifySliceChange(); 263 } 264 } 265 } 266