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