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.settingslib.volume; 18 19 import android.app.PendingIntent; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 import android.content.pm.PackageManager.NameNotFoundException; 25 import android.content.pm.ResolveInfo; 26 import android.media.IRemoteVolumeController; 27 import android.media.MediaMetadata; 28 import android.media.session.MediaController; 29 import android.media.session.MediaController.PlaybackInfo; 30 import android.media.session.MediaSession.QueueItem; 31 import android.media.session.MediaSession.Token; 32 import android.media.session.MediaSessionManager; 33 import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener; 34 import android.media.session.PlaybackState; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.os.Looper; 38 import android.os.Message; 39 import android.os.RemoteException; 40 import android.util.Log; 41 42 import java.io.PrintWriter; 43 import java.util.HashMap; 44 import java.util.HashSet; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Objects; 48 import java.util.Set; 49 50 /** 51 * Convenience client for all media session updates. Provides a callback interface for events 52 * related to remote media sessions. 53 */ 54 public class MediaSessions { 55 private static final String TAG = Util.logTag(MediaSessions.class); 56 57 private static final boolean USE_SERVICE_LABEL = false; 58 59 private final Context mContext; 60 private final H mHandler; 61 private final MediaSessionManager mMgr; 62 private final Map<Token, MediaControllerRecord> mRecords = new HashMap<>(); 63 private final Callbacks mCallbacks; 64 65 private boolean mInit; 66 MediaSessions(Context context, Looper looper, Callbacks callbacks)67 public MediaSessions(Context context, Looper looper, Callbacks callbacks) { 68 mContext = context; 69 mHandler = new H(looper); 70 mMgr = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); 71 mCallbacks = callbacks; 72 } 73 74 /** 75 * Dump to {@code writer} 76 */ dump(PrintWriter writer)77 public void dump(PrintWriter writer) { 78 writer.println(getClass().getSimpleName() + " state:"); 79 writer.print(" mInit: "); 80 writer.println(mInit); 81 writer.print(" mRecords.size: "); 82 writer.println(mRecords.size()); 83 int i = 0; 84 for (MediaControllerRecord r : mRecords.values()) { 85 dump(++i, writer, r.controller); 86 } 87 } 88 89 /** 90 * init MediaSessions 91 */ init()92 public void init() { 93 if (D.BUG) Log.d(TAG, "init"); 94 // will throw if no permission 95 mMgr.addOnActiveSessionsChangedListener(mSessionsListener, null, mHandler); 96 mInit = true; 97 postUpdateSessions(); 98 mMgr.registerRemoteVolumeController(mRvc); 99 } 100 postUpdateSessions()101 protected void postUpdateSessions() { 102 if (!mInit) return; 103 mHandler.sendEmptyMessage(H.UPDATE_SESSIONS); 104 } 105 106 /** 107 * Destroy MediaSessions 108 */ destroy()109 public void destroy() { 110 if (D.BUG) Log.d(TAG, "destroy"); 111 mInit = false; 112 mMgr.removeOnActiveSessionsChangedListener(mSessionsListener); 113 mMgr.unregisterRemoteVolumeController(mRvc); 114 } 115 116 /** 117 * Set volume {@code level} to remote media {@code token} 118 */ setVolume(Token token, int level)119 public void setVolume(Token token, int level) { 120 final MediaControllerRecord r = mRecords.get(token); 121 if (r == null) { 122 Log.w(TAG, "setVolume: No record found for token " + token); 123 return; 124 } 125 if (D.BUG) Log.d(TAG, "Setting level to " + level); 126 r.controller.setVolumeTo(level, 0); 127 } 128 onRemoteVolumeChangedH(Token sessionToken, int flags)129 private void onRemoteVolumeChangedH(Token sessionToken, int flags) { 130 final MediaController controller = new MediaController(mContext, sessionToken); 131 if (D.BUG) { 132 Log.d(TAG, "remoteVolumeChangedH " + controller.getPackageName() + " " 133 + Util.audioManagerFlagsToString(flags)); 134 } 135 final Token token = controller.getSessionToken(); 136 mCallbacks.onRemoteVolumeChanged(token, flags); 137 } 138 onUpdateRemoteControllerH(Token sessionToken)139 private void onUpdateRemoteControllerH(Token sessionToken) { 140 final MediaController controller = 141 sessionToken != null ? new MediaController(mContext, sessionToken) : null; 142 final String pkg = controller != null ? controller.getPackageName() : null; 143 if (D.BUG) Log.d(TAG, "updateRemoteControllerH " + pkg); 144 // this may be our only indication that a remote session is changed, refresh 145 postUpdateSessions(); 146 } 147 onActiveSessionsUpdatedH(List<MediaController> controllers)148 protected void onActiveSessionsUpdatedH(List<MediaController> controllers) { 149 if (D.BUG) Log.d(TAG, "onActiveSessionsUpdatedH n=" + controllers.size()); 150 final Set<Token> toRemove = new HashSet<Token>(mRecords.keySet()); 151 for (MediaController controller : controllers) { 152 final Token token = controller.getSessionToken(); 153 final PlaybackInfo pi = controller.getPlaybackInfo(); 154 toRemove.remove(token); 155 if (!mRecords.containsKey(token)) { 156 final MediaControllerRecord r = new MediaControllerRecord(controller); 157 r.name = getControllerName(controller); 158 mRecords.put(token, r); 159 controller.registerCallback(r, mHandler); 160 } 161 final MediaControllerRecord r = mRecords.get(token); 162 final boolean remote = isRemote(pi); 163 if (remote) { 164 updateRemoteH(token, r.name, pi); 165 r.sentRemote = true; 166 } 167 } 168 for (Token t : toRemove) { 169 final MediaControllerRecord r = mRecords.get(t); 170 r.controller.unregisterCallback(r); 171 mRecords.remove(t); 172 if (D.BUG) Log.d(TAG, "Removing " + r.name + " sentRemote=" + r.sentRemote); 173 if (r.sentRemote) { 174 mCallbacks.onRemoteRemoved(t); 175 r.sentRemote = false; 176 } 177 } 178 } 179 isRemote(PlaybackInfo pi)180 private static boolean isRemote(PlaybackInfo pi) { 181 return pi != null && pi.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE; 182 } 183 getControllerName(MediaController controller)184 protected String getControllerName(MediaController controller) { 185 final PackageManager pm = mContext.getPackageManager(); 186 final String pkg = controller.getPackageName(); 187 try { 188 if (USE_SERVICE_LABEL) { 189 final List<ResolveInfo> ris = pm.queryIntentServices( 190 new Intent("android.media.MediaRouteProviderService").setPackage(pkg), 0); 191 if (ris != null) { 192 for (ResolveInfo ri : ris) { 193 if (ri.serviceInfo == null) continue; 194 if (pkg.equals(ri.serviceInfo.packageName)) { 195 final String serviceLabel = 196 Objects.toString(ri.serviceInfo.loadLabel(pm), "").trim(); 197 if (serviceLabel.length() > 0) { 198 return serviceLabel; 199 } 200 } 201 } 202 } 203 } 204 final ApplicationInfo ai = pm.getApplicationInfo(pkg, 0); 205 final String appLabel = Objects.toString(ai.loadLabel(pm), "").trim(); 206 if (appLabel.length() > 0) { 207 return appLabel; 208 } 209 } catch (NameNotFoundException e) { 210 } 211 return pkg; 212 } 213 updateRemoteH(Token token, String name, PlaybackInfo pi)214 private void updateRemoteH(Token token, String name, PlaybackInfo pi) { 215 if (mCallbacks != null) { 216 mCallbacks.onRemoteUpdate(token, name, pi); 217 } 218 } 219 dump(int n, PrintWriter writer, MediaController c)220 private static void dump(int n, PrintWriter writer, MediaController c) { 221 writer.println(" Controller " + n + ": " + c.getPackageName()); 222 final Bundle extras = c.getExtras(); 223 final long flags = c.getFlags(); 224 final MediaMetadata mm = c.getMetadata(); 225 final PlaybackInfo pi = c.getPlaybackInfo(); 226 final PlaybackState playbackState = c.getPlaybackState(); 227 final List<QueueItem> queue = c.getQueue(); 228 final CharSequence queueTitle = c.getQueueTitle(); 229 final int ratingType = c.getRatingType(); 230 final PendingIntent sessionActivity = c.getSessionActivity(); 231 232 writer.println(" PlaybackState: " + Util.playbackStateToString(playbackState)); 233 writer.println(" PlaybackInfo: " + Util.playbackInfoToString(pi)); 234 if (mm != null) { 235 writer.println(" MediaMetadata.desc=" + mm.getDescription()); 236 } 237 writer.println(" RatingType: " + ratingType); 238 writer.println(" Flags: " + flags); 239 if (extras != null) { 240 writer.println(" Extras:"); 241 for (String key : extras.keySet()) { 242 writer.println(" " + key + "=" + extras.get(key)); 243 } 244 } 245 if (queueTitle != null) { 246 writer.println(" QueueTitle: " + queueTitle); 247 } 248 if (queue != null && !queue.isEmpty()) { 249 writer.println(" Queue:"); 250 for (QueueItem qi : queue) { 251 writer.println(" " + qi); 252 } 253 } 254 if (pi != null) { 255 writer.println(" sessionActivity: " + sessionActivity); 256 } 257 } 258 259 private final class MediaControllerRecord extends MediaController.Callback { 260 public final MediaController controller; 261 262 public boolean sentRemote; 263 public String name; 264 MediaControllerRecord(MediaController controller)265 private MediaControllerRecord(MediaController controller) { 266 this.controller = controller; 267 } 268 cb(String method)269 private String cb(String method) { 270 return method + " " + controller.getPackageName() + " "; 271 } 272 273 @Override onAudioInfoChanged(PlaybackInfo info)274 public void onAudioInfoChanged(PlaybackInfo info) { 275 if (D.BUG) { 276 Log.d(TAG, cb("onAudioInfoChanged") + Util.playbackInfoToString(info) 277 + " sentRemote=" + sentRemote); 278 } 279 final boolean remote = isRemote(info); 280 if (!remote && sentRemote) { 281 mCallbacks.onRemoteRemoved(controller.getSessionToken()); 282 sentRemote = false; 283 } else if (remote) { 284 updateRemoteH(controller.getSessionToken(), name, info); 285 sentRemote = true; 286 } 287 } 288 289 @Override onExtrasChanged(Bundle extras)290 public void onExtrasChanged(Bundle extras) { 291 if (D.BUG) Log.d(TAG, cb("onExtrasChanged") + extras); 292 } 293 294 @Override onMetadataChanged(MediaMetadata metadata)295 public void onMetadataChanged(MediaMetadata metadata) { 296 if (D.BUG) Log.d(TAG, cb("onMetadataChanged") + Util.mediaMetadataToString(metadata)); 297 } 298 299 @Override onPlaybackStateChanged(PlaybackState state)300 public void onPlaybackStateChanged(PlaybackState state) { 301 if (D.BUG) Log.d(TAG, cb("onPlaybackStateChanged") + Util.playbackStateToString(state)); 302 } 303 304 @Override onQueueChanged(List<QueueItem> queue)305 public void onQueueChanged(List<QueueItem> queue) { 306 if (D.BUG) Log.d(TAG, cb("onQueueChanged") + queue); 307 } 308 309 @Override onQueueTitleChanged(CharSequence title)310 public void onQueueTitleChanged(CharSequence title) { 311 if (D.BUG) Log.d(TAG, cb("onQueueTitleChanged") + title); 312 } 313 314 @Override onSessionDestroyed()315 public void onSessionDestroyed() { 316 if (D.BUG) Log.d(TAG, cb("onSessionDestroyed")); 317 } 318 319 @Override onSessionEvent(String event, Bundle extras)320 public void onSessionEvent(String event, Bundle extras) { 321 if (D.BUG) Log.d(TAG, cb("onSessionEvent") + "event=" + event + " extras=" + extras); 322 } 323 } 324 325 private final OnActiveSessionsChangedListener mSessionsListener = 326 new OnActiveSessionsChangedListener() { 327 @Override 328 public void onActiveSessionsChanged(List<MediaController> controllers) { 329 onActiveSessionsUpdatedH(controllers); 330 } 331 }; 332 333 private final IRemoteVolumeController mRvc = new IRemoteVolumeController.Stub() { 334 @Override 335 public void remoteVolumeChanged(Token sessionToken, int flags) 336 throws RemoteException { 337 mHandler.obtainMessage(H.REMOTE_VOLUME_CHANGED, flags, 0, 338 sessionToken).sendToTarget(); 339 } 340 341 @Override 342 public void updateRemoteController(final Token sessionToken) 343 throws RemoteException { 344 mHandler.obtainMessage(H.UPDATE_REMOTE_CONTROLLER, sessionToken).sendToTarget(); 345 } 346 }; 347 348 private final class H extends Handler { 349 private static final int UPDATE_SESSIONS = 1; 350 private static final int REMOTE_VOLUME_CHANGED = 2; 351 private static final int UPDATE_REMOTE_CONTROLLER = 3; 352 H(Looper looper)353 private H(Looper looper) { 354 super(looper); 355 } 356 357 @Override handleMessage(Message msg)358 public void handleMessage(Message msg) { 359 switch (msg.what) { 360 case UPDATE_SESSIONS: 361 onActiveSessionsUpdatedH(mMgr.getActiveSessions(null)); 362 break; 363 case REMOTE_VOLUME_CHANGED: 364 onRemoteVolumeChangedH((Token) msg.obj, msg.arg1); 365 break; 366 case UPDATE_REMOTE_CONTROLLER: 367 onUpdateRemoteControllerH((Token) msg.obj); 368 break; 369 } 370 } 371 } 372 373 /** 374 * Callback for remote media sessions 375 */ 376 public interface Callbacks { 377 /** 378 * Invoked when remote media session is updated 379 */ onRemoteUpdate(Token token, String name, PlaybackInfo pi)380 void onRemoteUpdate(Token token, String name, PlaybackInfo pi); 381 382 /** 383 * Invoked when remote media session is removed 384 */ onRemoteRemoved(Token t)385 void onRemoteRemoved(Token t); 386 387 /** 388 * Invoked when remote volume is changed 389 */ onRemoteVolumeChanged(Token token, int flags)390 void onRemoteVolumeChanged(Token token, int flags); 391 } 392 393 } 394