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