[go: nahoru, domu]

MediaBrowserServiceCompat.java revision 493571364635be0190cea8ee230a601070391e6f
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.Bundle;
24import android.os.Handler;
25import android.os.IBinder;
26import android.os.RemoteException;
27import android.support.annotation.NonNull;
28import android.support.annotation.Nullable;
29import android.support.v4.media.IMediaBrowserServiceCompat;
30import android.support.v4.media.IMediaBrowserServiceCompatCallbacks;
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.HashSet;
40import java.util.List;
41
42/**
43 * Base class for media browse services.
44 * <p>
45 * Media browse services enable applications to browse media content provided by an application
46 * and ask the application to start playing it.  They may also be used to control content that
47 * is already playing by way of a {@link MediaSessionCompat}.
48 * </p>
49 *
50 * To extend this class, you must declare the service in your manifest file with
51 * an intent filter with the {@link #SERVICE_INTERFACE} action.
52 *
53 * For example:
54 * </p><pre>
55 * &lt;service android:name=".MyMediaBrowserServiceCompat"
56 *          android:label="&#64;string/service_name" >
57 *     &lt;intent-filter>
58 *         &lt;action android:name="android.media.browse.MediaBrowserServiceCompat" />
59 *     &lt;/intent-filter>
60 * &lt;/service>
61 * </pre>
62 * @hide
63 */
64public abstract class MediaBrowserServiceCompat extends Service {
65    private static final String TAG = "MediaBrowserServiceCompat";
66    private static final boolean DBG = false;
67
68    /**
69     * The {@link Intent} that must be declared as handled by the service.
70     */
71    public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserServiceCompat";
72
73    /**
74     * A key for passing the MediaItem to the ResultReceiver in getItem.
75     *
76     * @hide
77     */
78    public static final String KEY_MEDIA_ITEM = "media_item";
79
80    private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap();
81    private final Handler mHandler = new Handler();
82    private ServiceBinder mBinder;
83    MediaSessionCompat.Token mSession;
84
85    /**
86     * All the info about a connection.
87     */
88    private class ConnectionRecord {
89        String pkg;
90        Bundle rootHints;
91        IMediaBrowserServiceCompatCallbacks callbacks;
92        BrowserRoot root;
93        HashSet<String> subscriptions = new HashSet();
94    }
95
96    /**
97     * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}.
98     * <p>
99     * Each of the methods that takes one of these to send the result must call
100     * {@link #sendResult} to respond to the caller with the given results.  If those
101     * functions return without calling {@link #sendResult}, they must instead call
102     * {@link #detach} before returning, and then may call {@link #sendResult} when
103     * they are done.  If more than one of those methods is called, an exception will
104     * be thrown.
105     *
106     * @see MediaBrowserServiceCompat#onLoadChildren
107     * @see MediaBrowserServiceCompat#onLoadItem
108     */
109    public class Result<T> {
110        private Object mDebug;
111        private boolean mDetachCalled;
112        private boolean mSendResultCalled;
113
114        Result(Object debug) {
115            mDebug = debug;
116        }
117
118        /**
119         * Send the result back to the caller.
120         */
121        public void sendResult(T result) {
122            if (mSendResultCalled) {
123                throw new IllegalStateException("sendResult() called twice for: " + mDebug);
124            }
125            mSendResultCalled = true;
126            onResultSent(result);
127        }
128
129        /**
130         * Detach this message from the current thread and allow the {@link #sendResult}
131         * call to happen later.
132         */
133        public void detach() {
134            if (mDetachCalled) {
135                throw new IllegalStateException("detach() called when detach() had already"
136                        + " been called for: " + mDebug);
137            }
138            if (mSendResultCalled) {
139                throw new IllegalStateException("detach() called when sendResult() had already"
140                        + " been called for: " + mDebug);
141            }
142            mDetachCalled = true;
143        }
144
145        boolean isDone() {
146            return mDetachCalled || mSendResultCalled;
147        }
148
149        /**
150         * Called when the result is sent, after assertions about not being called twice
151         * have happened.
152         */
153        void onResultSent(T result) {
154        }
155    }
156
157    private class ServiceBinder extends IMediaBrowserServiceCompat.Stub {
158        @Override
159        public void connect(final String pkg, final Bundle rootHints,
160                final IMediaBrowserServiceCompatCallbacks callbacks) {
161
162            final int uid = Binder.getCallingUid();
163            if (!isValidPackage(pkg, uid)) {
164                throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
165                        + " package=" + pkg);
166            }
167
168            mHandler.post(new Runnable() {
169                    @Override
170                    public void run() {
171                        final IBinder b = callbacks.asBinder();
172
173                        // Clear out the old subscriptions.  We are getting new ones.
174                        mConnections.remove(b);
175
176                        final ConnectionRecord connection = new ConnectionRecord();
177                        connection.pkg = pkg;
178                        connection.rootHints = rootHints;
179                        connection.callbacks = callbacks;
180
181                        connection.root =
182                                MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
183
184                        // If they didn't return something, don't allow this client.
185                        if (connection.root == null) {
186                            Log.i(TAG, "No root for client " + pkg + " from service "
187                                    + getClass().getName());
188                            try {
189                                callbacks.onConnectFailed();
190                            } catch (RemoteException ex) {
191                                Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
192                                        + "pkg=" + pkg);
193                            }
194                        } else {
195                            try {
196                                mConnections.put(b, connection);
197                                if (mSession != null) {
198                                    callbacks.onConnect(connection.root.getRootId(),
199                                            mSession, connection.root.getExtras());
200                                }
201                            } catch (RemoteException ex) {
202                                Log.w(TAG, "Calling onConnect() failed. Dropping client. "
203                                        + "pkg=" + pkg);
204                                mConnections.remove(b);
205                            }
206                        }
207                    }
208                });
209        }
210
211        @Override
212        public void disconnect(final IMediaBrowserServiceCompatCallbacks callbacks) {
213            mHandler.post(new Runnable() {
214                    @Override
215                    public void run() {
216                        final IBinder b = callbacks.asBinder();
217
218                        // Clear out the old subscriptions.  We are getting new ones.
219                        final ConnectionRecord old = mConnections.remove(b);
220                        if (old != null) {
221                            // TODO
222                        }
223                    }
224                });
225        }
226
227
228        @Override
229        public void addSubscription(
230                final String id, final IMediaBrowserServiceCompatCallbacks callbacks) {
231            mHandler.post(new Runnable() {
232                    @Override
233                    public void run() {
234                        final IBinder b = callbacks.asBinder();
235
236                        // Get the record for the connection
237                        final ConnectionRecord connection = mConnections.get(b);
238                        if (connection == null) {
239                            Log.w(TAG, "addSubscription for callback that isn't registered id="
240                                + id);
241                            return;
242                        }
243
244                        MediaBrowserServiceCompat.this.addSubscription(id, connection);
245                    }
246                });
247        }
248
249        @Override
250        public void removeSubscription(final String id,
251                final IMediaBrowserServiceCompatCallbacks callbacks) {
252            mHandler.post(new Runnable() {
253                @Override
254                public void run() {
255                    final IBinder b = callbacks.asBinder();
256
257                    ConnectionRecord connection = mConnections.get(b);
258                    if (connection == null) {
259                        Log.w(TAG, "removeSubscription for callback that isn't registered id="
260                                + id);
261                        return;
262                    }
263                    if (!connection.subscriptions.remove(id)) {
264                        Log.w(TAG, "removeSubscription called for " + id
265                                + " which is not subscribed");
266                    }
267                }
268            });
269        }
270
271        @Override
272        public void getMediaItem(final String mediaId, final ResultReceiver receiver) {
273            if (TextUtils.isEmpty(mediaId) || receiver == null) {
274                return;
275            }
276
277            mHandler.post(new Runnable() {
278                @Override
279                public void run() {
280                    performLoadItem(mediaId, receiver);
281                }
282            });
283        }
284    }
285
286    @Override
287    public void onCreate() {
288        super.onCreate();
289        mBinder = new ServiceBinder();
290    }
291
292    @Override
293    public IBinder onBind(Intent intent) {
294        if (SERVICE_INTERFACE.equals(intent.getAction())) {
295            return mBinder;
296        }
297        return null;
298    }
299
300    @Override
301    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
302    }
303
304    /**
305     * Called to get the root information for browsing by a particular client.
306     * <p>
307     * The implementation should verify that the client package has permission
308     * to access browse media information before returning the root id; it
309     * should return null if the client is not allowed to access this
310     * information.
311     * </p>
312     *
313     * @param clientPackageName The package name of the application which is
314     *            requesting access to browse media.
315     * @param clientUid The uid of the application which is requesting access to
316     *            browse media.
317     * @param rootHints An optional bundle of service-specific arguments to send
318     *            to the media browse service when connecting and retrieving the
319     *            root id for browsing, or null if none. The contents of this
320     *            bundle may affect the information returned when browsing.
321     * @return The {@link BrowserRoot} for accessing this app's content or null.
322     */
323    public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
324            int clientUid, @Nullable Bundle rootHints);
325
326    /**
327     * Called to get information about the children of a media item.
328     * <p>
329     * Implementations must call {@link Result#sendResult result.sendResult}
330     * with the list of children. If loading the children will be an expensive
331     * operation that should be performed on another thread,
332     * {@link Result#detach result.detach} may be called before returning from
333     * this function, and then {@link Result#sendResult result.sendResult}
334     * called when the loading is complete.
335     *
336     * @param parentId The id of the parent media item whose children are to be
337     *            queried.
338     * @param result The Result to send the list of children to, or null if the
339     *            id is invalid.
340     */
341    public abstract void onLoadChildren(@NonNull String parentId,
342            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result);
343
344    /**
345     * Called to get information about a specific media item.
346     * <p>
347     * Implementations must call {@link Result#sendResult result.sendResult}. If
348     * loading the item will be an expensive operation {@link Result#detach
349     * result.detach} may be called before returning from this function, and
350     * then {@link Result#sendResult result.sendResult} called when the item has
351     * been loaded.
352     * <p>
353     * The default implementation sends a null result.
354     *
355     * @param itemId The id for the specific
356     *            {@link MediaBrowserCompat.MediaItem}.
357     * @param result The Result to send the item to, or null if the id is
358     *            invalid.
359     */
360    public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) {
361        result.sendResult(null);
362    }
363
364    /**
365     * Call to set the media session.
366     * <p>
367     * This should be called as soon as possible during the service's startup.
368     * It may only be called once.
369     *
370     * @param token The token for the service's {@link MediaSessionCompat}.
371     */
372    public void setSessionToken(final MediaSessionCompat.Token token) {
373        if (token == null) {
374            throw new IllegalArgumentException("Session token may not be null.");
375        }
376        if (mSession != null) {
377            throw new IllegalStateException("The session token has already been set.");
378        }
379        mSession = token;
380        mHandler.post(new Runnable() {
381            @Override
382            public void run() {
383                for (IBinder key : mConnections.keySet()) {
384                    ConnectionRecord connection = mConnections.get(key);
385                    try {
386                        connection.callbacks.onConnect(connection.root.getRootId(), token,
387                                connection.root.getExtras());
388                    } catch (RemoteException e) {
389                        Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
390                        mConnections.remove(key);
391                    }
392                }
393            }
394        });
395    }
396
397    /**
398     * Gets the session token, or null if it has not yet been created
399     * or if it has been destroyed.
400     */
401    public @Nullable MediaSessionCompat.Token getSessionToken() {
402        return mSession;
403    }
404
405    /**
406     * Notifies all connected media browsers that the children of
407     * the specified parent id have changed in some way.
408     * This will cause browsers to fetch subscribed content again.
409     *
410     * @param parentId The id of the parent media item whose
411     * children changed.
412     */
413    public void notifyChildrenChanged(@NonNull final String parentId) {
414        if (parentId == null) {
415            throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
416        }
417        mHandler.post(new Runnable() {
418            @Override
419            public void run() {
420                for (IBinder binder : mConnections.keySet()) {
421                    ConnectionRecord connection = mConnections.get(binder);
422                    if (connection.subscriptions.contains(parentId)) {
423                        performLoadChildren(parentId, connection);
424                    }
425                }
426            }
427        });
428    }
429
430    /**
431     * Return whether the given package is one of the ones that is owned by the uid.
432     */
433    private boolean isValidPackage(String pkg, int uid) {
434        if (pkg == null) {
435            return false;
436        }
437        final PackageManager pm = getPackageManager();
438        final String[] packages = pm.getPackagesForUid(uid);
439        final int N = packages.length;
440        for (int i=0; i<N; i++) {
441            if (packages[i].equals(pkg)) {
442                return true;
443            }
444        }
445        return false;
446    }
447
448    /**
449     * Save the subscription and if it is a new subscription send the results.
450     */
451    private void addSubscription(String id, ConnectionRecord connection) {
452        // Save the subscription
453        connection.subscriptions.add(id);
454
455        // send the results
456        performLoadChildren(id, connection);
457    }
458
459    /**
460     * Call onLoadChildren and then send the results back to the connection.
461     * <p>
462     * Callers must make sure that this connection is still connected.
463     */
464    private void performLoadChildren(final String parentId, final ConnectionRecord connection) {
465        final Result<List<MediaBrowserCompat.MediaItem>> result
466                = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
467            @Override
468            void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
469                if (list == null) {
470                    throw new IllegalStateException("onLoadChildren sent null list for id "
471                            + parentId);
472                }
473                if (mConnections.get(connection.callbacks.asBinder()) != connection) {
474                    if (DBG) {
475                        Log.d(TAG, "Not sending onLoadChildren result for connection that has"
476                                + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
477                    }
478                    return;
479                }
480
481                try {
482                    connection.callbacks.onLoadChildren(parentId, list);
483                } catch (RemoteException ex) {
484                    // The other side is in the process of crashing.
485                    Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
486                            + " package=" + connection.pkg);
487                }
488            }
489        };
490
491        onLoadChildren(parentId, result);
492
493        if (!result.isDone()) {
494            throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
495                    + " before returning for package=" + connection.pkg + " id=" + parentId);
496        }
497    }
498
499    private void performLoadItem(String itemId, final ResultReceiver receiver) {
500        final Result<MediaBrowserCompat.MediaItem> result =
501                new Result<MediaBrowserCompat.MediaItem>(itemId) {
502            @Override
503            void onResultSent(MediaBrowserCompat.MediaItem item) {
504                Bundle bundle = new Bundle();
505                bundle.putParcelable(KEY_MEDIA_ITEM, item);
506                receiver.send(0, bundle);
507            }
508        };
509
510        MediaBrowserServiceCompat.this.onLoadItem(itemId, result);
511
512        if (!result.isDone()) {
513            throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
514                    + " before returning for id=" + itemId);
515        }
516    }
517
518    /**
519     * Contains information that the browser service needs to send to the client
520     * when first connected.
521     */
522    public static final class BrowserRoot {
523        final private String mRootId;
524        final private Bundle mExtras;
525
526        /**
527         * Constructs a browser root.
528         * @param rootId The root id for browsing.
529         * @param extras Any extras about the browser service.
530         */
531        public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
532            if (rootId == null) {
533                throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
534                        "Use null for BrowserRoot instead.");
535            }
536            mRootId = rootId;
537            mExtras = extras;
538        }
539
540        /**
541         * Gets the root id for browsing.
542         */
543        public String getRootId() {
544            return mRootId;
545        }
546
547        /**
548         * Gets any extras about the brwoser service.
549         */
550        public Bundle getExtras() {
551            return mExtras;
552        }
553    }
554}
555