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