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