[go: nahoru, domu]

MediaBrowserServiceCompat.java revision 9703a1e215168b6b580430ec490ca616b6490c80
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 android.support.v4.media;
18
19import android.app.Service;
20import android.content.Intent;
21import android.content.pm.PackageManager;
22import android.os.Binder;
23import android.os.Build;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.IBinder;
27import android.os.Parcel;
28import android.os.RemoteException;
29import android.support.annotation.NonNull;
30import android.support.annotation.Nullable;
31import android.support.v4.media.session.MediaSessionCompat;
32import android.support.v4.os.ResultReceiver;
33import android.support.v4.util.ArrayMap;
34import android.text.TextUtils;
35import android.util.Log;
36
37import java.io.FileDescriptor;
38import java.io.PrintWriter;
39import java.util.ArrayList;
40import java.util.HashSet;
41import java.util.List;
42
43/**
44 * Base class for media browse services.
45 * <p>
46 * Media browse services enable applications to browse media content provided by an application
47 * and ask the application to start playing it.  They may also be used to control content that
48 * is already playing by way of a {@link MediaSessionCompat}.
49 * </p>
50 *
51 * To extend this class, you must declare the service in your manifest file with
52 * an intent filter with the {@link #SERVICE_INTERFACE} action.
53 *
54 * For example:
55 * </p><pre>
56 * &lt;service android:name=".MyMediaBrowserServiceCompat"
57 *          android:label="&#64;string/service_name" >
58 *     &lt;intent-filter>
59 *         &lt;action android:name="android.media.browse.MediaBrowserService" />
60 *     &lt;/intent-filter>
61 * &lt;/service>
62 * </pre>
63 * @hide
64 */
65public abstract class MediaBrowserServiceCompat extends Service {
66    private static final String TAG = "MediaBrowserServiceCompat";
67    private static final boolean DBG = false;
68
69    private MediaBrowserServiceImpl mImpl;
70
71    /**
72     * The {@link Intent} that must be declared as handled by the service.
73     */
74    public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
75
76    /**
77     * A key for passing the MediaItem to the ResultReceiver in getItem.
78     *
79     * @hide
80     */
81    public static final String KEY_MEDIA_ITEM = "media_item";
82
83    private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap();
84    private final Handler mHandler = new Handler();
85    MediaSessionCompat.Token mSession;
86
87    interface MediaBrowserServiceImpl {
88        public void onCreate();
89        IBinder onBind(Intent intent);
90    }
91
92    class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl {
93        private ServiceBinderCompat mBinder;
94
95        @Override
96        public void onCreate() {
97            mBinder = new ServiceBinderCompat(new ServiceStub());
98        }
99
100        @Override
101        public IBinder onBind(Intent intent) {
102            // STOPSHIP: Use messenger or version management for further extension
103            if (SERVICE_INTERFACE.equals(intent.getAction())) {
104                return mBinder;
105            }
106            return null;
107        }
108    }
109
110    class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl {
111        private Object mServiceObj;
112
113        @Override
114        public void onCreate() {
115            mServiceObj = MediaBrowserServiceCompatApi21.createService();
116            MediaBrowserServiceCompatApi21.onCreate(mServiceObj,
117                    new ServiceStubApi21(new ServiceStub()));
118        }
119
120        @Override
121        public IBinder onBind(Intent intent) {
122            return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent);
123        }
124    }
125
126    /**
127     * All the info about a connection.
128     */
129    private class ConnectionRecord {
130        String pkg;
131        Bundle rootHints;
132        ServiceCallbacks callbacks;
133        BrowserRoot root;
134        HashSet<String> subscriptions = new HashSet();
135    }
136
137    /**
138     * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}.
139     * <p>
140     * Each of the methods that takes one of these to send the result must call
141     * {@link #sendResult} to respond to the caller with the given results.  If those
142     * functions return without calling {@link #sendResult}, they must instead call
143     * {@link #detach} before returning, and then may call {@link #sendResult} when
144     * they are done.  If more than one of those methods is called, an exception will
145     * be thrown.
146     *
147     * @see MediaBrowserServiceCompat#onLoadChildren
148     * @see MediaBrowserServiceCompat#onLoadItem
149     */
150    public static class Result<T> {
151        private Object mDebug;
152        private boolean mDetachCalled;
153        private boolean mSendResultCalled;
154
155        Result(Object debug) {
156            mDebug = debug;
157        }
158
159        /**
160         * Send the result back to the caller.
161         */
162        public void sendResult(T result) {
163            if (mSendResultCalled) {
164                throw new IllegalStateException("sendResult() called twice for: " + mDebug);
165            }
166            mSendResultCalled = true;
167            onResultSent(result);
168        }
169
170        /**
171         * Detach this message from the current thread and allow the {@link #sendResult}
172         * call to happen later.
173         */
174        public void detach() {
175            if (mDetachCalled) {
176                throw new IllegalStateException("detach() called when detach() had already"
177                        + " been called for: " + mDebug);
178            }
179            if (mSendResultCalled) {
180                throw new IllegalStateException("detach() called when sendResult() had already"
181                        + " been called for: " + mDebug);
182            }
183            mDetachCalled = true;
184        }
185
186        boolean isDone() {
187            return mDetachCalled || mSendResultCalled;
188        }
189
190        /**
191         * Called when the result is sent, after assertions about not being called twice
192         * have happened.
193         */
194        void onResultSent(T result) {
195        }
196    }
197
198    private class ServiceStub {
199        public void connect(final String pkg, final Bundle rootHints,
200                final ServiceCallbacks callbacks) {
201
202            final int uid = Binder.getCallingUid();
203            if (!isValidPackage(pkg, uid)) {
204                throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
205                        + " package=" + pkg);
206            }
207
208            mHandler.post(new Runnable() {
209                @Override
210                public void run() {
211                    final IBinder b = callbacks.asBinder();
212
213                    // Clear out the old subscriptions.  We are getting new ones.
214                    mConnections.remove(b);
215
216                    final ConnectionRecord connection = new ConnectionRecord();
217                    connection.pkg = pkg;
218                    connection.rootHints = rootHints;
219                    connection.callbacks = callbacks;
220
221                    connection.root =
222                            MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
223
224                    // If they didn't return something, don't allow this client.
225                    if (connection.root == null) {
226                        Log.i(TAG, "No root for client " + pkg + " from service "
227                                + getClass().getName());
228                        try {
229                            callbacks.onConnectFailed();
230                        } catch (RemoteException ex) {
231                            Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
232                                    + "pkg=" + pkg);
233                        }
234                    } else {
235                        try {
236                            mConnections.put(b, connection);
237                            if (mSession != null) {
238                                callbacks.onConnect(connection.root.getRootId(),
239                                        mSession, connection.root.getExtras());
240                            }
241                        } catch (RemoteException ex) {
242                            Log.w(TAG, "Calling onConnect() failed. Dropping client. "
243                                    + "pkg=" + pkg);
244                            mConnections.remove(b);
245                        }
246                    }
247                }
248            });
249        }
250
251        public void disconnect(final ServiceCallbacks callbacks) {
252            mHandler.post(new Runnable() {
253                @Override
254                public void run() {
255                    final IBinder b = callbacks.asBinder();
256
257                    // Clear out the old subscriptions.  We are getting new ones.
258                    final ConnectionRecord old = mConnections.remove(b);
259                    if (old != null) {
260                        // TODO
261                    }
262                }
263            });
264        }
265
266
267        public void addSubscription(final String id, final ServiceCallbacks callbacks) {
268            mHandler.post(new Runnable() {
269                @Override
270                public void run() {
271                    final IBinder b = callbacks.asBinder();
272
273                    // Get the record for the connection
274                    final ConnectionRecord connection = mConnections.get(b);
275                    if (connection == null) {
276                        Log.w(TAG, "addSubscription for callback that isn't registered id="
277                                + id);
278                        return;
279                    }
280
281                    MediaBrowserServiceCompat.this.addSubscription(id, connection);
282                }
283            });
284        }
285
286        public void removeSubscription(final String id, final ServiceCallbacks callbacks) {
287            mHandler.post(new Runnable() {
288                @Override
289                public void run() {
290                    final IBinder b = callbacks.asBinder();
291
292                    ConnectionRecord connection = mConnections.get(b);
293                    if (connection == null) {
294                        Log.w(TAG, "removeSubscription for callback that isn't registered id="
295                                + id);
296                        return;
297                    }
298                    if (!connection.subscriptions.remove(id)) {
299                        Log.w(TAG, "removeSubscription called for " + id
300                                + " which is not subscribed");
301                    }
302                }
303            });
304        }
305
306        public void getMediaItem(final String mediaId, final ResultReceiver receiver) {
307            if (TextUtils.isEmpty(mediaId) || receiver == null) {
308                return;
309            }
310
311            mHandler.post(new Runnable() {
312                @Override
313                public void run() {
314                    performLoadItem(mediaId, receiver);
315                }
316            });
317        }
318    }
319
320    private class ServiceBinderCompat extends IMediaBrowserServiceCompat.Stub {
321        final ServiceStub mServiceStub;
322
323        ServiceBinderCompat(ServiceStub binder) {
324            mServiceStub = binder;
325        }
326
327        @Override
328        public void connect(final String pkg, final Bundle rootHints,
329                final IMediaBrowserServiceCompatCallbacks callbacks) {
330            mServiceStub.connect(pkg, rootHints, new ServiceCallbacksCompat(callbacks));
331        }
332
333        @Override
334        public void disconnect(final IMediaBrowserServiceCompatCallbacks callbacks) {
335            mConnections.get(callbacks.asBinder());
336            mServiceStub.disconnect(new ServiceCallbacksCompat(callbacks));
337        }
338
339
340        @Override
341        public void addSubscription(
342                final String id, final IMediaBrowserServiceCompatCallbacks callbacks) {
343            mServiceStub.addSubscription(id, new ServiceCallbacksCompat(callbacks));
344        }
345
346        @Override
347        public void removeSubscription(final String id,
348                final IMediaBrowserServiceCompatCallbacks callbacks) {
349            mServiceStub.removeSubscription(id, new ServiceCallbacksCompat(callbacks));
350        }
351
352        @Override
353        public void getMediaItem(final String mediaId, final ResultReceiver receiver) {
354            mServiceStub.getMediaItem(mediaId, receiver);
355        }
356    }
357
358    private class ServiceStubApi21 implements MediaBrowserServiceCompatApi21.ServiceStub {
359        final ServiceStub mBinder;
360
361        ServiceStubApi21(ServiceStub binder) {
362            mBinder = binder;
363        }
364
365        @Override
366        public void connect(final String pkg, final Bundle rootHints,
367                final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
368            mBinder.connect(pkg, rootHints, new ServiceCallbacksApi21(callbacks));
369        }
370
371        @Override
372        public void disconnect(final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
373            mBinder.disconnect(new ServiceCallbacksApi21(callbacks));
374        }
375
376
377        @Override
378        public void addSubscription(
379                final String id, final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
380            mBinder.addSubscription(id, new ServiceCallbacksApi21(callbacks));
381        }
382
383        @Override
384        public void removeSubscription(final String id,
385                final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
386            mBinder.removeSubscription(id, new ServiceCallbacksApi21(callbacks));
387        }
388
389        @Override
390        public void getMediaItem(final String mediaId, final android.os.ResultReceiver receiver) {
391            ResultReceiver receiverCompat = new ResultReceiver(mHandler) {
392                @Override
393                protected void onReceiveResult(int resultCode, Bundle resultData) {
394                    receiver.send(resultCode, resultData);
395                }
396            };
397            mBinder.getMediaItem(mediaId, receiverCompat);
398        }
399    }
400
401    private interface ServiceCallbacks {
402        IBinder asBinder();
403        void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
404                throws RemoteException;
405        void onConnectFailed() throws RemoteException;
406        void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list)
407                throws RemoteException;
408    }
409
410    private class ServiceCallbacksCompat implements ServiceCallbacks {
411        final IMediaBrowserServiceCompatCallbacks mCallbacks;
412
413        ServiceCallbacksCompat(IMediaBrowserServiceCompatCallbacks callbacks) {
414            mCallbacks = callbacks;
415        }
416
417        public IBinder asBinder() {
418            return mCallbacks.asBinder();
419        }
420
421        public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
422                throws RemoteException {
423            mCallbacks.onConnect(root, session, extras);
424        }
425
426        public void onConnectFailed() throws RemoteException {
427            mCallbacks.onConnectFailed();
428        }
429
430        public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list)
431                throws RemoteException {
432            mCallbacks.onLoadChildren(mediaId, list);
433        }
434    }
435
436    private class ServiceCallbacksApi21 implements ServiceCallbacks {
437        final MediaBrowserServiceCompatApi21.ServiceCallbacks mCallbacks;
438
439        ServiceCallbacksApi21(MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
440            mCallbacks = callbacks;
441        }
442
443        public IBinder asBinder() {
444            return mCallbacks.asBinder();
445        }
446
447        public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
448                throws RemoteException {
449            mCallbacks.onConnect(root, session.getToken(), extras);
450        }
451
452        public void onConnectFailed() throws RemoteException {
453            mCallbacks.onConnectFailed();
454        }
455
456        public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list)
457                throws RemoteException {
458            List<Parcel> parcelList = null;
459            if (list != null) {
460                parcelList = new ArrayList<>();
461                for (MediaBrowserCompat.MediaItem item : list) {
462                    Parcel parcel = Parcel.obtain();
463                    item.writeToParcel(parcel, 0);
464                    parcelList.add(parcel);
465                }
466            }
467            mCallbacks.onLoadChildren(mediaId, parcelList);
468        }
469    }
470
471    @Override
472    public void onCreate() {
473        super.onCreate();
474        if (Build.VERSION.SDK_INT >= 21) {
475            mImpl = new MediaBrowserServiceImplApi21();
476        } else {
477            mImpl = new MediaBrowserServiceImplBase();
478        }
479        mImpl.onCreate();
480    }
481
482    @Override
483    public IBinder onBind(Intent intent) {
484        return mImpl.onBind(intent);
485    }
486
487    @Override
488    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
489    }
490
491    /**
492     * Called to get the root information for browsing by a particular client.
493     * <p>
494     * The implementation should verify that the client package has permission
495     * to access browse media information before returning the root id; it
496     * should return null if the client is not allowed to access this
497     * information.
498     * </p>
499     *
500     * @param clientPackageName The package name of the application which is
501     *            requesting access to browse media.
502     * @param clientUid The uid of the application which is requesting access to
503     *            browse media.
504     * @param rootHints An optional bundle of service-specific arguments to send
505     *            to the media browse service when connecting and retrieving the
506     *            root id for browsing, or null if none. The contents of this
507     *            bundle may affect the information returned when browsing.
508     * @return The {@link BrowserRoot} for accessing this app's content or null.
509     */
510    public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
511            int clientUid, @Nullable Bundle rootHints);
512
513    /**
514     * Called to get information about the children of a media item.
515     * <p>
516     * Implementations must call {@link Result#sendResult result.sendResult}
517     * with the list of children. If loading the children will be an expensive
518     * operation that should be performed on another thread,
519     * {@link Result#detach result.detach} may be called before returning from
520     * this function, and then {@link Result#sendResult result.sendResult}
521     * called when the loading is complete.
522     *
523     * @param parentId The id of the parent media item whose children are to be
524     *            queried.
525     * @param result The Result to send the list of children to, or null if the
526     *            id is invalid.
527     */
528    public abstract void onLoadChildren(@NonNull String parentId,
529            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result);
530
531    /**
532     * Called to get information about a specific media item.
533     * <p>
534     * Implementations must call {@link Result#sendResult result.sendResult}. If
535     * loading the item will be an expensive operation {@link Result#detach
536     * result.detach} may be called before returning from this function, and
537     * then {@link Result#sendResult result.sendResult} called when the item has
538     * been loaded.
539     * <p>
540     * The default implementation sends a null result.
541     *
542     * @param itemId The id for the specific
543     *            {@link MediaBrowserCompat.MediaItem}.
544     * @param result The Result to send the item to, or null if the id is
545     *            invalid.
546     */
547    public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) {
548        result.sendResult(null);
549    }
550
551    /**
552     * Call to set the media session.
553     * <p>
554     * This should be called as soon as possible during the service's startup.
555     * It may only be called once.
556     *
557     * @param token The token for the service's {@link MediaSessionCompat}.
558     */
559    public void setSessionToken(final MediaSessionCompat.Token token) {
560        if (token == null) {
561            throw new IllegalArgumentException("Session token may not be null.");
562        }
563        if (mSession != null) {
564            throw new IllegalStateException("The session token has already been set.");
565        }
566        mSession = token;
567        mHandler.post(new Runnable() {
568            @Override
569            public void run() {
570                for (IBinder key : mConnections.keySet()) {
571                    ConnectionRecord connection = mConnections.get(key);
572                    try {
573                        connection.callbacks.onConnect(connection.root.getRootId(), token,
574                                connection.root.getExtras());
575                    } catch (RemoteException e) {
576                        Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
577                        mConnections.remove(key);
578                    }
579                }
580            }
581        });
582    }
583
584    /**
585     * Gets the session token, or null if it has not yet been created
586     * or if it has been destroyed.
587     */
588    public @Nullable MediaSessionCompat.Token getSessionToken() {
589        return mSession;
590    }
591
592    /**
593     * Notifies all connected media browsers that the children of
594     * the specified parent id have changed in some way.
595     * This will cause browsers to fetch subscribed content again.
596     *
597     * @param parentId The id of the parent media item whose
598     * children changed.
599     */
600    public void notifyChildrenChanged(@NonNull final String parentId) {
601        if (parentId == null) {
602            throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
603        }
604        mHandler.post(new Runnable() {
605            @Override
606            public void run() {
607                for (IBinder binder : mConnections.keySet()) {
608                    ConnectionRecord connection = mConnections.get(binder);
609                    if (connection.subscriptions.contains(parentId)) {
610                        performLoadChildren(parentId, connection);
611                    }
612                }
613            }
614        });
615    }
616
617    /**
618     * Return whether the given package is one of the ones that is owned by the uid.
619     */
620    private boolean isValidPackage(String pkg, int uid) {
621        if (pkg == null) {
622            return false;
623        }
624        final PackageManager pm = getPackageManager();
625        final String[] packages = pm.getPackagesForUid(uid);
626        final int N = packages.length;
627        for (int i=0; i<N; i++) {
628            if (packages[i].equals(pkg)) {
629                return true;
630            }
631        }
632        return false;
633    }
634
635    /**
636     * Save the subscription and if it is a new subscription send the results.
637     */
638    private void addSubscription(String id, ConnectionRecord connection) {
639        // Save the subscription
640        connection.subscriptions.add(id);
641
642        // send the results
643        performLoadChildren(id, connection);
644    }
645
646    /**
647     * Call onLoadChildren and then send the results back to the connection.
648     * <p>
649     * Callers must make sure that this connection is still connected.
650     */
651    private void performLoadChildren(final String parentId, final ConnectionRecord connection) {
652        final Result<List<MediaBrowserCompat.MediaItem>> result
653                = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
654            @Override
655            void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
656                if (list == null) {
657                    throw new IllegalStateException("onLoadChildren sent null list for id "
658                            + parentId);
659                }
660                if (mConnections.get(connection.callbacks.asBinder()) != connection) {
661                    if (DBG) {
662                        Log.d(TAG, "Not sending onLoadChildren result for connection that has"
663                                + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
664                    }
665                    return;
666                }
667
668                try {
669                    connection.callbacks.onLoadChildren(parentId, list);
670                } catch (RemoteException ex) {
671                    // The other side is in the process of crashing.
672                    Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
673                            + " package=" + connection.pkg);
674                }
675            }
676        };
677
678        onLoadChildren(parentId, result);
679
680        if (!result.isDone()) {
681            throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
682                    + " before returning for package=" + connection.pkg + " id=" + parentId);
683        }
684    }
685
686    private void performLoadItem(String itemId, final ResultReceiver receiver) {
687        final Result<MediaBrowserCompat.MediaItem> result =
688                new Result<MediaBrowserCompat.MediaItem>(itemId) {
689            @Override
690            void onResultSent(MediaBrowserCompat.MediaItem item) {
691                Bundle bundle = new Bundle();
692                bundle.putParcelable(KEY_MEDIA_ITEM, item);
693                receiver.send(0, bundle);
694            }
695        };
696
697        MediaBrowserServiceCompat.this.onLoadItem(itemId, result);
698
699        if (!result.isDone()) {
700            throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
701                    + " before returning for id=" + itemId);
702        }
703    }
704
705    /**
706     * Contains information that the browser service needs to send to the client
707     * when first connected.
708     */
709    public static final class BrowserRoot {
710        final private String mRootId;
711        final private Bundle mExtras;
712
713        /**
714         * Constructs a browser root.
715         * @param rootId The root id for browsing.
716         * @param extras Any extras about the browser service.
717         */
718        public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
719            if (rootId == null) {
720                throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
721                        "Use null for BrowserRoot instead.");
722            }
723            mRootId = rootId;
724            mExtras = extras;
725        }
726
727        /**
728         * Gets the root id for browsing.
729         */
730        public String getRootId() {
731            return mRootId;
732        }
733
734        /**
735         * Gets any extras about the brwoser service.
736         */
737        public Bundle getExtras() {
738            return mExtras;
739        }
740    }
741}
742