MediaBrowserServiceCompat.java revision 68284cf61cf46c69e443536b350b6cab9debbcdd
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 static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
21import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
22import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT;
23import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
24import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
25import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
26import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
27import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
28import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLING_UID;
29import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
30import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
31import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
32import static android.support.v4.media.MediaBrowserProtocol.DATA_OPTIONS;
33import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
34import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
35import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
36import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
37import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
38import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
39import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
40import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED;
41import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN;
42import static android.support.v4.media.MediaBrowserProtocol.SERVICE_VERSION_CURRENT;
43
44import android.app.Service;
45import android.content.Intent;
46import android.content.pm.PackageManager;
47import android.os.Binder;
48import android.os.Build;
49import android.os.Bundle;
50import android.os.Handler;
51import android.os.IBinder;
52import android.os.Message;
53import android.os.Messenger;
54import android.os.Parcel;
55import android.os.RemoteException;
56import android.support.annotation.IntDef;
57import android.support.annotation.NonNull;
58import android.support.annotation.Nullable;
59import android.support.annotation.RestrictTo;
60import android.support.v4.app.BundleCompat;
61import android.support.v4.media.session.MediaSessionCompat;
62import android.support.v4.os.BuildCompat;
63import android.support.v4.os.ResultReceiver;
64import android.support.v4.util.ArrayMap;
65import android.support.v4.util.Pair;
66import android.text.TextUtils;
67import android.util.Log;
68
69import java.io.FileDescriptor;
70import java.io.PrintWriter;
71import java.lang.annotation.Retention;
72import java.lang.annotation.RetentionPolicy;
73import java.util.ArrayList;
74import java.util.Collections;
75import java.util.HashMap;
76import java.util.Iterator;
77import java.util.List;
78
79/**
80 * Base class for media browse services.
81 * <p>
82 * Media browse services enable applications to browse media content provided by an application
83 * and ask the application to start playing it. They may also be used to control content that
84 * is already playing by way of a {@link MediaSessionCompat}.
85 * </p>
86 *
87 * To extend this class, you must declare the service in your manifest file with
88 * an intent filter with the {@link #SERVICE_INTERFACE} action.
89 *
90 * For example:
91 * </p><pre>
92 * &lt;service android:name=".MyMediaBrowserServiceCompat"
93 *          android:label="&#64;string/service_name" >
94 *     &lt;intent-filter>
95 *         &lt;action android:name="android.media.browse.MediaBrowserService" />
96 *     &lt;/intent-filter>
97 * &lt;/service>
98 * </pre>
99 */
100public abstract class MediaBrowserServiceCompat extends Service {
101    static final String TAG = "MBServiceCompat";
102    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
103
104    private MediaBrowserServiceImpl mImpl;
105
106    /**
107     * The {@link Intent} that must be declared as handled by the service.
108     */
109    public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
110
111    /**
112     * A key for passing the MediaItem to the ResultReceiver in getItem.
113     *
114     * @hide
115     */
116    @RestrictTo(LIBRARY_GROUP)
117    public static final String KEY_MEDIA_ITEM = "media_item";
118
119    static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001;
120    static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 0x00000002;
121
122    /** @hide */
123    @RestrictTo(LIBRARY_GROUP)
124    @Retention(RetentionPolicy.SOURCE)
125    @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
126            RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED })
127    private @interface ResultFlags { }
128
129    final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
130    ConnectionRecord mCurConnection;
131    final ServiceHandler mHandler = new ServiceHandler();
132    MediaSessionCompat.Token mSession;
133
134    interface MediaBrowserServiceImpl {
135        void onCreate();
136        IBinder onBind(Intent intent);
137        void setSessionToken(MediaSessionCompat.Token token);
138        void notifyChildrenChanged(final String parentId, final Bundle options);
139        Bundle getBrowserRootHints();
140    }
141
142    class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl {
143        private Messenger mMessenger;
144
145        @Override
146        public void onCreate() {
147            mMessenger = new Messenger(mHandler);
148        }
149
150        @Override
151        public IBinder onBind(Intent intent) {
152            if (SERVICE_INTERFACE.equals(intent.getAction())) {
153                return mMessenger.getBinder();
154            }
155            return null;
156        }
157
158        @Override
159        public void setSessionToken(final MediaSessionCompat.Token token) {
160            mHandler.post(new Runnable() {
161                @Override
162                public void run() {
163                    Iterator<ConnectionRecord> iter = mConnections.values().iterator();
164                    while (iter.hasNext()){
165                        ConnectionRecord connection = iter.next();
166                        try {
167                            connection.callbacks.onConnect(connection.root.getRootId(), token,
168                                    connection.root.getExtras());
169                        } catch (RemoteException e) {
170                            Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
171                            iter.remove();
172                        }
173                    }
174                }
175            });
176        }
177
178        @Override
179        public void notifyChildrenChanged(@NonNull final String parentId, final Bundle options) {
180            mHandler.post(new Runnable() {
181                @Override
182                public void run() {
183                    for (IBinder binder : mConnections.keySet()) {
184                        ConnectionRecord connection = mConnections.get(binder);
185                        List<Pair<IBinder, Bundle>> callbackList =
186                                connection.subscriptions.get(parentId);
187                        if (callbackList != null) {
188                            for (Pair<IBinder, Bundle> callback : callbackList) {
189                                if (MediaBrowserCompatUtils.hasDuplicatedItems(
190                                        options, callback.second)) {
191                                    performLoadChildren(parentId, connection, callback.second);
192                                }
193                            }
194                        }
195                    }
196                }
197            });
198        }
199
200        @Override
201        public Bundle getBrowserRootHints() {
202            if (mCurConnection == null) {
203                throw new IllegalStateException("This should be called inside of onLoadChildren or"
204                        + " onLoadItem methods");
205            }
206            return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
207        }
208    }
209
210    class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl,
211            MediaBrowserServiceCompatApi21.ServiceCompatProxy {
212        Object mServiceObj;
213        Messenger mMessenger;
214
215        @Override
216        public void onCreate() {
217            mServiceObj = MediaBrowserServiceCompatApi21.createService(
218                    MediaBrowserServiceCompat.this, this);
219            MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
220        }
221
222        @Override
223        public IBinder onBind(Intent intent) {
224            return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent);
225        }
226
227        @Override
228        public void setSessionToken(MediaSessionCompat.Token token) {
229            MediaBrowserServiceCompatApi21.setSessionToken(mServiceObj, token.getToken());
230        }
231
232        @Override
233        public void notifyChildrenChanged(final String parentId, final Bundle options) {
234            if (mMessenger == null) {
235                MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
236            } else {
237                mHandler.post(new Runnable() {
238                    @Override
239                    public void run() {
240                        for (IBinder binder : mConnections.keySet()) {
241                            ConnectionRecord connection = mConnections.get(binder);
242                            List<Pair<IBinder, Bundle>> callbackList =
243                                    connection.subscriptions.get(parentId);
244                            if (callbackList != null) {
245                                for (Pair<IBinder, Bundle> callback : callbackList) {
246                                    if (MediaBrowserCompatUtils.hasDuplicatedItems(
247                                            options, callback.second)) {
248                                        performLoadChildren(parentId, connection, callback.second);
249                                    }
250                                }
251                            }
252                        }
253                    }
254                });
255            }
256        }
257
258        @Override
259        public Bundle getBrowserRootHints() {
260            if (mMessenger == null) {
261                // TODO: Handle getBrowserRootHints when connected with framework MediaBrowser.
262                return null;
263            }
264            if (mCurConnection == null) {
265                throw new IllegalStateException("This should be called inside of onLoadChildren or"
266                        + " onLoadItem methods");
267            }
268            return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
269        }
270
271        @Override
272        public MediaBrowserServiceCompatApi21.BrowserRoot onGetRoot(
273                String clientPackageName, int clientUid, Bundle rootHints) {
274            Bundle rootExtras = null;
275            if (rootHints != null && rootHints.getInt(EXTRA_CLIENT_VERSION, 0) != 0) {
276                rootHints.remove(EXTRA_CLIENT_VERSION);
277                mMessenger = new Messenger(mHandler);
278                rootExtras = new Bundle();
279                rootExtras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
280                BundleCompat.putBinder(rootExtras, EXTRA_MESSENGER_BINDER, mMessenger.getBinder());
281            }
282            BrowserRoot root = MediaBrowserServiceCompat.this.onGetRoot(
283                    clientPackageName, clientUid, rootHints);
284            if (root == null) {
285                return null;
286            }
287            if (rootExtras == null) {
288                rootExtras = root.getExtras();
289            } else if (root.getExtras() != null) {
290                rootExtras.putAll(root.getExtras());
291            }
292            return new MediaBrowserServiceCompatApi21.BrowserRoot(
293                    root.getRootId(), rootExtras);
294        }
295
296        @Override
297        public void onLoadChildren(String parentId,
298                final MediaBrowserServiceCompatApi21.ResultWrapper<List<Parcel>> resultWrapper) {
299            final Result<List<MediaBrowserCompat.MediaItem>> result
300                    = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
301                @Override
302                void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flags) {
303                    List<Parcel> parcelList = null;
304                    if (list != null) {
305                        parcelList = new ArrayList<>();
306                        for (MediaBrowserCompat.MediaItem item : list) {
307                            Parcel parcel = Parcel.obtain();
308                            item.writeToParcel(parcel, 0);
309                            parcelList.add(parcel);
310                        }
311                    }
312                    resultWrapper.sendResult(parcelList);
313                }
314
315                @Override
316                public void detach() {
317                    resultWrapper.detach();
318                }
319            };
320            MediaBrowserServiceCompat.this.onLoadChildren(parentId, result);
321        }
322    }
323
324    class MediaBrowserServiceImplApi23 extends MediaBrowserServiceImplApi21 implements
325            MediaBrowserServiceCompatApi23.ServiceCompatProxy {
326        @Override
327        public void onCreate() {
328            mServiceObj = MediaBrowserServiceCompatApi23.createService(
329                    MediaBrowserServiceCompat.this, this);
330            MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
331        }
332
333        @Override
334        public void onLoadItem(String itemId,
335                final MediaBrowserServiceCompatApi21.ResultWrapper<Parcel> resultWrapper) {
336            final Result<MediaBrowserCompat.MediaItem> result
337                    = new Result<MediaBrowserCompat.MediaItem>(itemId) {
338                @Override
339                void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flags) {
340                    if (item == null) {
341                        resultWrapper.sendResult(null);
342                    } else {
343                        Parcel parcelItem = Parcel.obtain();
344                        item.writeToParcel(parcelItem, 0);
345                        resultWrapper.sendResult(parcelItem);
346                    }
347                }
348
349                @Override
350                public void detach() {
351                    resultWrapper.detach();
352                }
353            };
354            MediaBrowserServiceCompat.this.onLoadItem(itemId, result);
355        }
356    }
357
358    class MediaBrowserServiceImplApi24 extends MediaBrowserServiceImplApi23 implements
359            MediaBrowserServiceCompatApi24.ServiceCompatProxy {
360        @Override
361        public void onCreate() {
362            mServiceObj = MediaBrowserServiceCompatApi24.createService(
363                    MediaBrowserServiceCompat.this, this);
364            MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
365        }
366
367        @Override
368        public void notifyChildrenChanged(final String parentId, final Bundle options) {
369            if (options == null) {
370                MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
371            } else {
372                MediaBrowserServiceCompatApi24.notifyChildrenChanged(mServiceObj, parentId,
373                        options);
374            }
375        }
376
377        @Override
378        public void onLoadChildren(String parentId,
379                final MediaBrowserServiceCompatApi24.ResultWrapper resultWrapper, Bundle options) {
380            final Result<List<MediaBrowserCompat.MediaItem>> result
381                    = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
382                @Override
383                void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flags) {
384                    List<Parcel> parcelList = null;
385                    if (list != null) {
386                        parcelList = new ArrayList<>();
387                        for (MediaBrowserCompat.MediaItem item : list) {
388                            Parcel parcel = Parcel.obtain();
389                            item.writeToParcel(parcel, 0);
390                            parcelList.add(parcel);
391                        }
392                    }
393                    resultWrapper.sendResult(parcelList, flags);
394                }
395
396                @Override
397                public void detach() {
398                    resultWrapper.detach();
399                }
400            };
401            MediaBrowserServiceCompat.this.onLoadChildren(parentId, result, options);
402        }
403
404        @Override
405        public Bundle getBrowserRootHints() {
406            // If EXTRA_MESSENGER_BINDER is used, mCurConnection is not null.
407            if (mCurConnection != null) {
408                return mCurConnection.rootHints == null ? null
409                        : new Bundle(mCurConnection.rootHints);
410            }
411            return MediaBrowserServiceCompatApi24.getBrowserRootHints(mServiceObj);
412        }
413    }
414
415    private final class ServiceHandler extends Handler {
416        private final ServiceBinderImpl mServiceBinderImpl = new ServiceBinderImpl();
417
418        ServiceHandler() {
419        }
420
421        @Override
422        public void handleMessage(Message msg) {
423            Bundle data = msg.getData();
424            switch (msg.what) {
425                case CLIENT_MSG_CONNECT:
426                    mServiceBinderImpl.connect(data.getString(DATA_PACKAGE_NAME),
427                            data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS),
428                            new ServiceCallbacksCompat(msg.replyTo));
429                    break;
430                case CLIENT_MSG_DISCONNECT:
431                    mServiceBinderImpl.disconnect(new ServiceCallbacksCompat(msg.replyTo));
432                    break;
433                case CLIENT_MSG_ADD_SUBSCRIPTION:
434                    mServiceBinderImpl.addSubscription(data.getString(DATA_MEDIA_ITEM_ID),
435                            BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN),
436                            data.getBundle(DATA_OPTIONS),
437                            new ServiceCallbacksCompat(msg.replyTo));
438                    break;
439                case CLIENT_MSG_REMOVE_SUBSCRIPTION:
440                    mServiceBinderImpl.removeSubscription(data.getString(DATA_MEDIA_ITEM_ID),
441                            BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN),
442                            new ServiceCallbacksCompat(msg.replyTo));
443                    break;
444                case CLIENT_MSG_GET_MEDIA_ITEM:
445                    mServiceBinderImpl.getMediaItem(data.getString(DATA_MEDIA_ITEM_ID),
446                            (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER),
447                            new ServiceCallbacksCompat(msg.replyTo));
448                    break;
449                case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER:
450                    mServiceBinderImpl.registerCallbacks(new ServiceCallbacksCompat(msg.replyTo),
451                            data.getBundle(DATA_ROOT_HINTS));
452                    break;
453                case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER:
454                    mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo));
455                    break;
456                default:
457                    Log.w(TAG, "Unhandled message: " + msg
458                            + "\n  Service version: " + SERVICE_VERSION_CURRENT
459                            + "\n  Client version: " + msg.arg1);
460            }
461        }
462
463        @Override
464        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
465            // Binder.getCallingUid() in handleMessage will return the uid of this process.
466            // In order to get the right calling uid, Binder.getCallingUid() should be called here.
467            Bundle data = msg.getData();
468            data.setClassLoader(MediaBrowserCompat.class.getClassLoader());
469            data.putInt(DATA_CALLING_UID, Binder.getCallingUid());
470            return super.sendMessageAtTime(msg, uptimeMillis);
471        }
472
473        public void postOrRun(Runnable r) {
474            if (Thread.currentThread() == getLooper().getThread()) {
475                r.run();
476            } else {
477                post(r);
478            }
479        }
480    }
481
482    /**
483     * All the info about a connection.
484     */
485    private class ConnectionRecord {
486        String pkg;
487        Bundle rootHints;
488        ServiceCallbacks callbacks;
489        BrowserRoot root;
490        HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap();
491
492        ConnectionRecord() {
493        }
494    }
495
496    /**
497     * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}.
498     * <p>
499     * Each of the methods that takes one of these to send the result must call
500     * {@link #sendResult} to respond to the caller with the given results. If those
501     * functions return without calling {@link #sendResult}, they must instead call
502     * {@link #detach} before returning, and then may call {@link #sendResult} when
503     * they are done. If more than one of those methods is called, an exception will
504     * be thrown.
505     *
506     * @see MediaBrowserServiceCompat#onLoadChildren
507     * @see MediaBrowserServiceCompat#onLoadItem
508     */
509    public static class Result<T> {
510        private Object mDebug;
511        private boolean mDetachCalled;
512        private boolean mSendResultCalled;
513        private int mFlags;
514
515        Result(Object debug) {
516            mDebug = debug;
517        }
518
519        /**
520         * Send the result back to the caller.
521         */
522        public void sendResult(T result) {
523            if (mSendResultCalled) {
524                throw new IllegalStateException("sendResult() called twice for: " + mDebug);
525            }
526            mSendResultCalled = true;
527            onResultSent(result, mFlags);
528        }
529
530        /**
531         * Detach this message from the current thread and allow the {@link #sendResult}
532         * call to happen later.
533         */
534        public void detach() {
535            if (mDetachCalled) {
536                throw new IllegalStateException("detach() called when detach() had already"
537                        + " been called for: " + mDebug);
538            }
539            if (mSendResultCalled) {
540                throw new IllegalStateException("detach() called when sendResult() had already"
541                        + " been called for: " + mDebug);
542            }
543            mDetachCalled = true;
544        }
545
546        boolean isDone() {
547            return mDetachCalled || mSendResultCalled;
548        }
549
550        void setFlags(@ResultFlags int flags) {
551            mFlags = flags;
552        }
553
554        /**
555         * Called when the result is sent, after assertions about not being called twice
556         * have happened.
557         */
558        void onResultSent(T result, @ResultFlags int flags) {
559        }
560    }
561
562    private class ServiceBinderImpl {
563        ServiceBinderImpl() {
564        }
565
566        public void connect(final String pkg, final int uid, final Bundle rootHints,
567                final ServiceCallbacks callbacks) {
568
569            if (!isValidPackage(pkg, uid)) {
570                throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
571                        + " package=" + pkg);
572            }
573
574            mHandler.postOrRun(new Runnable() {
575                @Override
576                public void run() {
577                    final IBinder b = callbacks.asBinder();
578
579                    // Clear out the old subscriptions. We are getting new ones.
580                    mConnections.remove(b);
581
582                    final ConnectionRecord connection = new ConnectionRecord();
583                    connection.pkg = pkg;
584                    connection.rootHints = rootHints;
585                    connection.callbacks = callbacks;
586
587                    connection.root =
588                            MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
589
590                    // If they didn't return something, don't allow this client.
591                    if (connection.root == null) {
592                        Log.i(TAG, "No root for client " + pkg + " from service "
593                                + getClass().getName());
594                        try {
595                            callbacks.onConnectFailed();
596                        } catch (RemoteException ex) {
597                            Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
598                                    + "pkg=" + pkg);
599                        }
600                    } else {
601                        try {
602                            mConnections.put(b, connection);
603                            if (mSession != null) {
604                                callbacks.onConnect(connection.root.getRootId(),
605                                        mSession, connection.root.getExtras());
606                            }
607                        } catch (RemoteException ex) {
608                            Log.w(TAG, "Calling onConnect() failed. Dropping client. "
609                                    + "pkg=" + pkg);
610                            mConnections.remove(b);
611                        }
612                    }
613                }
614            });
615        }
616
617        public void disconnect(final ServiceCallbacks callbacks) {
618            mHandler.postOrRun(new Runnable() {
619                @Override
620                public void run() {
621                    final IBinder b = callbacks.asBinder();
622
623                    // Clear out the old subscriptions. We are getting new ones.
624                    final ConnectionRecord old = mConnections.remove(b);
625                    if (old != null) {
626                        // TODO
627                    }
628                }
629            });
630        }
631
632        public void addSubscription(final String id, final IBinder token, final Bundle options,
633                final ServiceCallbacks callbacks) {
634            mHandler.postOrRun(new Runnable() {
635                @Override
636                public void run() {
637                    final IBinder b = callbacks.asBinder();
638
639                    // Get the record for the connection
640                    final ConnectionRecord connection = mConnections.get(b);
641                    if (connection == null) {
642                        Log.w(TAG, "addSubscription for callback that isn't registered id="
643                                + id);
644                        return;
645                    }
646
647                    MediaBrowserServiceCompat.this.addSubscription(id, connection, token, options);
648                }
649            });
650        }
651
652        public void removeSubscription(final String id, final IBinder token,
653                final ServiceCallbacks callbacks) {
654            mHandler.postOrRun(new Runnable() {
655                @Override
656                public void run() {
657                    final IBinder b = callbacks.asBinder();
658
659                    ConnectionRecord connection = mConnections.get(b);
660                    if (connection == null) {
661                        Log.w(TAG, "removeSubscription for callback that isn't registered id="
662                                + id);
663                        return;
664                    }
665                    if (!MediaBrowserServiceCompat.this.removeSubscription(
666                            id, connection, token)) {
667                        Log.w(TAG, "removeSubscription called for " + id
668                                + " which is not subscribed");
669                    }
670                }
671            });
672        }
673
674        public void getMediaItem(final String mediaId, final ResultReceiver receiver,
675                final ServiceCallbacks callbacks) {
676            if (TextUtils.isEmpty(mediaId) || receiver == null) {
677                return;
678            }
679
680            mHandler.postOrRun(new Runnable() {
681                @Override
682                public void run() {
683                    final IBinder b = callbacks.asBinder();
684
685                    ConnectionRecord connection = mConnections.get(b);
686                    if (connection == null) {
687                        Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId);
688                        return;
689                    }
690                    performLoadItem(mediaId, connection, receiver);
691                }
692            });
693        }
694
695        // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used.
696        public void registerCallbacks(final ServiceCallbacks callbacks, final Bundle rootHints) {
697            mHandler.postOrRun(new Runnable() {
698                @Override
699                public void run() {
700                    final IBinder b = callbacks.asBinder();
701                    // Clear out the old subscriptions. We are getting new ones.
702                    mConnections.remove(b);
703
704                    final ConnectionRecord connection = new ConnectionRecord();
705                    connection.callbacks = callbacks;
706                    connection.rootHints = rootHints;
707                    mConnections.put(b, connection);
708                }
709            });
710        }
711
712        // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used.
713        public void unregisterCallbacks(final ServiceCallbacks callbacks) {
714            mHandler.postOrRun(new Runnable() {
715                @Override
716                public void run() {
717                    final IBinder b = callbacks.asBinder();
718                    mConnections.remove(b);
719                }
720            });
721        }
722    }
723
724    private interface ServiceCallbacks {
725        IBinder asBinder();
726        void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
727                throws RemoteException;
728        void onConnectFailed() throws RemoteException;
729        void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options)
730                throws RemoteException;
731    }
732
733    private class ServiceCallbacksCompat implements ServiceCallbacks {
734        final Messenger mCallbacks;
735
736        ServiceCallbacksCompat(Messenger callbacks) {
737            mCallbacks = callbacks;
738        }
739
740        @Override
741        public IBinder asBinder() {
742            return mCallbacks.getBinder();
743        }
744
745        @Override
746        public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
747                throws RemoteException {
748            if (extras == null) {
749                extras = new Bundle();
750            }
751            extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
752            Bundle data = new Bundle();
753            data.putString(DATA_MEDIA_ITEM_ID, root);
754            data.putParcelable(DATA_MEDIA_SESSION_TOKEN, session);
755            data.putBundle(DATA_ROOT_HINTS, extras);
756            sendRequest(SERVICE_MSG_ON_CONNECT, data);
757        }
758
759        @Override
760        public void onConnectFailed() throws RemoteException {
761            sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null);
762        }
763
764        @Override
765        public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list,
766                Bundle options) throws RemoteException {
767            Bundle data = new Bundle();
768            data.putString(DATA_MEDIA_ITEM_ID, mediaId);
769            data.putBundle(DATA_OPTIONS, options);
770            if (list != null) {
771                data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST,
772                        list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list));
773            }
774            sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data);
775        }
776
777        private void sendRequest(int what, Bundle data) throws RemoteException {
778            Message msg = Message.obtain();
779            msg.what = what;
780            msg.arg1 = SERVICE_VERSION_CURRENT;
781            msg.setData(data);
782            mCallbacks.send(msg);
783        }
784    }
785
786    @Override
787    public void onCreate() {
788        super.onCreate();
789        if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) {
790            mImpl = new MediaBrowserServiceImplApi24();
791        } else if (Build.VERSION.SDK_INT >= 23) {
792            mImpl = new MediaBrowserServiceImplApi23();
793        } else if (Build.VERSION.SDK_INT >= 21) {
794            mImpl = new MediaBrowserServiceImplApi21();
795        } else {
796            mImpl = new MediaBrowserServiceImplBase();
797        }
798        mImpl.onCreate();
799    }
800
801    @Override
802    public IBinder onBind(Intent intent) {
803        return mImpl.onBind(intent);
804    }
805
806    @Override
807    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
808    }
809
810    /**
811     * Called to get the root information for browsing by a particular client.
812     * <p>
813     * The implementation should verify that the client package has permission
814     * to access browse media information before returning the root id; it
815     * should return null if the client is not allowed to access this
816     * information.
817     * </p>
818     *
819     * @param clientPackageName The package name of the application which is
820     *            requesting access to browse media.
821     * @param clientUid The uid of the application which is requesting access to
822     *            browse media.
823     * @param rootHints An optional bundle of service-specific arguments to send
824     *            to the media browse service when connecting and retrieving the
825     *            root id for browsing, or null if none. The contents of this
826     *            bundle may affect the information returned when browsing.
827     * @return The {@link BrowserRoot} for accessing this app's content or null.
828     * @see BrowserRoot#EXTRA_RECENT
829     * @see BrowserRoot#EXTRA_OFFLINE
830     * @see BrowserRoot#EXTRA_SUGGESTED
831     * @see BrowserRoot#EXTRA_SUGGESTION_KEYWORDS
832     */
833    public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
834            int clientUid, @Nullable Bundle rootHints);
835
836    /**
837     * Called to get information about the children of a media item.
838     * <p>
839     * Implementations must call {@link Result#sendResult result.sendResult}
840     * with the list of children. If loading the children will be an expensive
841     * operation that should be performed on another thread,
842     * {@link Result#detach result.detach} may be called before returning from
843     * this function, and then {@link Result#sendResult result.sendResult}
844     * called when the loading is complete.
845     * </p><p>
846     * In case the media item does not have any children, call {@link Result#sendResult}
847     * with an empty list. When the given {@code parentId} is invalid, implementations must
848     * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
849     * {@link MediaBrowserCompat.SubscriptionCallback#onError}.
850     * </p>
851     *
852     * @param parentId The id of the parent media item whose children are to be
853     *            queried.
854     * @param result The Result to send the list of children to.
855     */
856    public abstract void onLoadChildren(@NonNull String parentId,
857            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result);
858
859    /**
860     * Called to get information about the children of a media item.
861     * <p>
862     * Implementations must call {@link Result#sendResult result.sendResult}
863     * with the list of children. If loading the children will be an expensive
864     * operation that should be performed on another thread,
865     * {@link Result#detach result.detach} may be called before returning from
866     * this function, and then {@link Result#sendResult result.sendResult}
867     * called when the loading is complete.
868     * </p><p>
869     * In case the media item does not have any children, call {@link Result#sendResult}
870     * with an empty list. When the given {@code parentId} is invalid, implementations must
871     * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
872     * {@link MediaBrowserCompat.SubscriptionCallback#onError}.
873     * </p>
874     *
875     * @param parentId The id of the parent media item whose children are to be
876     *            queried.
877     * @param result The Result to send the list of children to.
878     * @param options A bundle of service-specific arguments sent from the media
879     *            browse. The information returned through the result should be
880     *            affected by the contents of this bundle.
881     */
882    public void onLoadChildren(@NonNull String parentId,
883            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options) {
884        // To support backward compatibility, when the implementation of MediaBrowserService doesn't
885        // override onLoadChildren() with options, onLoadChildren() without options will be used
886        // instead, and the options will be applied in the implementation of result.onResultSent().
887        result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED);
888        onLoadChildren(parentId, result);
889    }
890
891    /**
892     * Called to get information about a specific media item.
893     * <p>
894     * Implementations must call {@link Result#sendResult result.sendResult}. If
895     * loading the item will be an expensive operation {@link Result#detach
896     * result.detach} may be called before returning from this function, and
897     * then {@link Result#sendResult result.sendResult} called when the item has
898     * been loaded.
899     * </p><p>
900     * When the given {@code itemId} is invalid, implementations must call
901     * {@link Result#sendResult result.sendResult} with {@code null}.
902     * </p><p>
903     * The default implementation will invoke {@link MediaBrowserCompat.ItemCallback#onError}.
904     *
905     * @param itemId The id for the specific {@link MediaBrowserCompat.MediaItem}.
906     * @param result The Result to send the item to, or null if the id is
907     *            invalid.
908     */
909    public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) {
910        result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED);
911        result.sendResult(null);
912    }
913
914    /**
915     * Call to set the media session.
916     * <p>
917     * This should be called as soon as possible during the service's startup.
918     * It may only be called once.
919     *
920     * @param token The token for the service's {@link MediaSessionCompat}.
921     */
922    public void setSessionToken(MediaSessionCompat.Token token) {
923        if (token == null) {
924            throw new IllegalArgumentException("Session token may not be null.");
925        }
926        if (mSession != null) {
927            throw new IllegalStateException("The session token has already been set.");
928        }
929        mSession = token;
930        mImpl.setSessionToken(token);
931    }
932
933    /**
934     * Gets the session token, or null if it has not yet been created
935     * or if it has been destroyed.
936     */
937    public @Nullable MediaSessionCompat.Token getSessionToken() {
938        return mSession;
939    }
940
941    /**
942     * Gets the root hints sent from the currently connected {@link MediaBrowserCompat}.
943     * The root hints are service-specific arguments included in an optional bundle sent to the
944     * media browser service when connecting and retrieving the root id for browsing, or null if
945     * none. The contents of this bundle may affect the information returned when browsing.
946     * <p>
947     * Note that this will return null when connected to {@link android.media.browse.MediaBrowser}
948     * and running on API 23 or lower.
949     *
950     * @throws IllegalStateException If this method is called outside of {@link #onLoadChildren}
951     *             or {@link #onLoadItem}
952     * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
953     * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
954     * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
955     * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTION_KEYWORDS
956     */
957    public final Bundle getBrowserRootHints() {
958        return mImpl.getBrowserRootHints();
959    }
960
961    /**
962     * Notifies all connected media browsers that the children of
963     * the specified parent id have changed in some way.
964     * This will cause browsers to fetch subscribed content again.
965     *
966     * @param parentId The id of the parent media item whose
967     * children changed.
968     */
969    public void notifyChildrenChanged(@NonNull String parentId) {
970        if (parentId == null) {
971            throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
972        }
973        mImpl.notifyChildrenChanged(parentId, null);
974    }
975
976    /**
977     * Notifies all connected media browsers that the children of
978     * the specified parent id have changed in some way.
979     * This will cause browsers to fetch subscribed content again.
980     *
981     * @param parentId The id of the parent media item whose
982     *            children changed.
983     * @param options A bundle of service-specific arguments to send
984     *            to the media browse. The contents of this bundle may
985     *            contain the information about the change.
986     */
987    public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) {
988        if (parentId == null) {
989            throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
990        }
991        if (options == null) {
992            throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged");
993        }
994        mImpl.notifyChildrenChanged(parentId, options);
995    }
996
997    /**
998     * Return whether the given package is one of the ones that is owned by the uid.
999     */
1000    boolean isValidPackage(String pkg, int uid) {
1001        if (pkg == null) {
1002            return false;
1003        }
1004        final PackageManager pm = getPackageManager();
1005        final String[] packages = pm.getPackagesForUid(uid);
1006        final int N = packages.length;
1007        for (int i=0; i<N; i++) {
1008            if (packages[i].equals(pkg)) {
1009                return true;
1010            }
1011        }
1012        return false;
1013    }
1014
1015    /**
1016     * Save the subscription and if it is a new subscription send the results.
1017     */
1018    void addSubscription(String id, ConnectionRecord connection, IBinder token,
1019            Bundle options) {
1020        // Save the subscription
1021        List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
1022        if (callbackList == null) {
1023            callbackList = new ArrayList<>();
1024        }
1025        for (Pair<IBinder, Bundle> callback : callbackList) {
1026            if (token == callback.first
1027                    && MediaBrowserCompatUtils.areSameOptions(options, callback.second)) {
1028                return;
1029            }
1030        }
1031        callbackList.add(new Pair<>(token, options));
1032        connection.subscriptions.put(id, callbackList);
1033        // send the results
1034        performLoadChildren(id, connection, options);
1035    }
1036
1037    /**
1038     * Remove the subscription.
1039     */
1040    boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) {
1041        if (token == null) {
1042            return connection.subscriptions.remove(id) != null;
1043        }
1044        boolean removed = false;
1045        List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
1046        if (callbackList != null) {
1047            Iterator<Pair<IBinder, Bundle>> iter = callbackList.iterator();
1048            while (iter.hasNext()){
1049                if (token == iter.next().first) {
1050                    removed = true;
1051                    iter.remove();
1052                }
1053            }
1054            if (callbackList.size() == 0) {
1055                connection.subscriptions.remove(id);
1056            }
1057        }
1058        return removed;
1059    }
1060
1061    /**
1062     * Call onLoadChildren and then send the results back to the connection.
1063     * <p>
1064     * Callers must make sure that this connection is still connected.
1065     */
1066    void performLoadChildren(final String parentId, final ConnectionRecord connection,
1067            final Bundle options) {
1068        final Result<List<MediaBrowserCompat.MediaItem>> result
1069                = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
1070            @Override
1071            void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flags) {
1072                if (mConnections.get(connection.callbacks.asBinder()) != connection) {
1073                    if (DEBUG) {
1074                        Log.d(TAG, "Not sending onLoadChildren result for connection that has"
1075                                + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
1076                    }
1077                    return;
1078                }
1079
1080                List<MediaBrowserCompat.MediaItem> filteredList =
1081                        (flags & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
1082                                ? applyOptions(list, options) : list;
1083                try {
1084                    connection.callbacks.onLoadChildren(parentId, filteredList, options);
1085                } catch (RemoteException ex) {
1086                    // The other side is in the process of crashing.
1087                    Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
1088                            + " package=" + connection.pkg);
1089                }
1090            }
1091        };
1092
1093        mCurConnection = connection;
1094        if (options == null) {
1095            onLoadChildren(parentId, result);
1096        } else {
1097            onLoadChildren(parentId, result, options);
1098        }
1099        mCurConnection = null;
1100
1101        if (!result.isDone()) {
1102            throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
1103                    + " before returning for package=" + connection.pkg + " id=" + parentId);
1104        }
1105    }
1106
1107    List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list,
1108            final Bundle options) {
1109        if (list == null) {
1110            return null;
1111        }
1112        int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
1113        int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
1114        if (page == -1 && pageSize == -1) {
1115            return list;
1116        }
1117        int fromIndex = pageSize * page;
1118        int toIndex = fromIndex + pageSize;
1119        if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
1120            return Collections.EMPTY_LIST;
1121        }
1122        if (toIndex > list.size()) {
1123            toIndex = list.size();
1124        }
1125        return list.subList(fromIndex, toIndex);
1126    }
1127
1128    void performLoadItem(String itemId, ConnectionRecord connection,
1129            final ResultReceiver receiver) {
1130        final Result<MediaBrowserCompat.MediaItem> result =
1131                new Result<MediaBrowserCompat.MediaItem>(itemId) {
1132                    @Override
1133                    void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flags) {
1134                        if ((flags & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) {
1135                            receiver.send(-1, null);
1136                            return;
1137                        }
1138                        Bundle bundle = new Bundle();
1139                        bundle.putParcelable(KEY_MEDIA_ITEM, item);
1140                        receiver.send(0, bundle);
1141                    }
1142                };
1143
1144        mCurConnection = connection;
1145        onLoadItem(itemId, result);
1146        mCurConnection = null;
1147
1148        if (!result.isDone()) {
1149            throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
1150                    + " before returning for id=" + itemId);
1151        }
1152    }
1153
1154    /**
1155     * Contains information that the browser service needs to send to the client
1156     * when first connected.
1157     */
1158    public static final class BrowserRoot {
1159        /**
1160         * The lookup key for a boolean that indicates whether the browser service should return a
1161         * browser root for recently played media items.
1162         *
1163         * <p>When creating a media browser for a given media browser service, this key can be
1164         * supplied as a root hint for retrieving media items that are recently played.
1165         * If the media browser service can provide such media items, the implementation must return
1166         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1167         *
1168         * <p>The root hint may contain multiple keys.
1169         *
1170         * @see #EXTRA_OFFLINE
1171         * @see #EXTRA_SUGGESTED
1172         * @see #EXTRA_SUGGESTION_KEYWORDS
1173         */
1174        public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
1175
1176        /**
1177         * The lookup key for a boolean that indicates whether the browser service should return a
1178         * browser root for offline media items.
1179         *
1180         * <p>When creating a media browser for a given media browser service, this key can be
1181         * supplied as a root hint for retrieving media items that are can be played without an
1182         * internet connection.
1183         * If the media browser service can provide such media items, the implementation must return
1184         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1185         *
1186         * <p>The root hint may contain multiple keys.
1187         *
1188         * @see #EXTRA_RECENT
1189         * @see #EXTRA_SUGGESTED
1190         * @see #EXTRA_SUGGESTION_KEYWORDS
1191         */
1192        public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
1193
1194        /**
1195         * The lookup key for a boolean that indicates whether the browser service should return a
1196         * browser root for suggested media items.
1197         *
1198         * <p>When creating a media browser for a given media browser service, this key can be
1199         * supplied as a root hint for retrieving the media items suggested by the media browser
1200         * service. The list of media items passed in {@link android.support.v4.media.MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded(String, List)}
1201         * is considered ordered by relevance, first being the top suggestion.
1202         * If the media browser service can provide such media items, the implementation must return
1203         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1204         *
1205         * <p>The root hint may contain multiple keys.
1206         *
1207         * @see #EXTRA_RECENT
1208         * @see #EXTRA_OFFLINE
1209         * @see #EXTRA_SUGGESTION_KEYWORDS
1210         */
1211        public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
1212
1213        /**
1214         * The lookup key for a string that indicates specific keywords which will be considered
1215         * when the browser service suggests media items.
1216         *
1217         * <p>When creating a media browser for a given media browser service, this key can be
1218         * supplied as a root hint together with {@link #EXTRA_SUGGESTED} for retrieving suggested
1219         * media items related with the keywords. The list of media items passed in
1220         * {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
1221         * is considered ordered by relevance, first being the top suggestion.
1222         * If the media browser service can provide such media items, the implementation must return
1223         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1224         *
1225         * <p>The root hint may contain multiple keys.
1226         *
1227         * @see #EXTRA_RECENT
1228         * @see #EXTRA_OFFLINE
1229         * @see #EXTRA_SUGGESTED
1230         */
1231        public static final String EXTRA_SUGGESTION_KEYWORDS
1232                = "android.service.media.extra.SUGGESTION_KEYWORDS";
1233
1234        final private String mRootId;
1235        final private Bundle mExtras;
1236
1237        /**
1238         * Constructs a browser root.
1239         * @param rootId The root id for browsing.
1240         * @param extras Any extras about the browser service.
1241         */
1242        public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
1243            if (rootId == null) {
1244                throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
1245                        "Use null for BrowserRoot instead.");
1246            }
1247            mRootId = rootId;
1248            mExtras = extras;
1249        }
1250
1251        /**
1252         * Gets the root id for browsing.
1253         */
1254        public String getRootId() {
1255            return mRootId;
1256        }
1257
1258        /**
1259         * Gets any extras about the browser service.
1260         */
1261        public Bundle getExtras() {
1262            return mExtras;
1263        }
1264    }
1265}
1266