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