MediaRouteProvider.java revision b507e525a61ed761eecfc2eaaf19af7e8db5dca5
1/*
2 * Copyright (C) 2013 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.v7.media;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.IntentFilter;
22import android.graphics.drawable.Drawable;
23import android.os.Bundle;
24import android.os.Handler;
25import android.os.Message;
26import android.os.Parcelable;
27import android.support.v7.media.MediaRouter.ControlRequestCallback;
28import android.text.TextUtils;
29
30import java.util.concurrent.CopyOnWriteArrayList;
31
32/**
33 * Media route providers are used to publish additional media routes for
34 * use within an application.  Media route providers may also be declared
35 * as a service to publish additional media routes to all applications
36 * in the system.
37 * <p>
38 * Applications and services should extend this class to publish additional media routes
39 * to the {@link MediaRouter}.  To make additional media routes available within
40 * your application, call {@link MediaRouter#addProvider} to add your provider to
41 * the media router.  To make additional media routes available to all applications
42 * in the system, register a media route provider service in your manifest.
43 * </p><p>
44 * This object must only be accessed on the main thread.
45 * </p>
46 */
47public abstract class MediaRouteProvider {
48    private static final int MSG_DELIVER_DESCRIPTOR_CHANGED = 1;
49
50    private final Context mContext;
51    private final ProviderMetadata mMetadata;
52    private final ProviderHandler mHandler = new ProviderHandler();
53    private final CopyOnWriteArrayList<Callback> mCallbacks =
54            new CopyOnWriteArrayList<Callback>();
55
56    private ProviderDescriptor mDescriptor;
57    private boolean mPendingDescriptorChange;
58
59    /**
60     * Creates a media route provider.
61     *
62     * @param context The context.
63     */
64    public MediaRouteProvider(Context context) {
65        this(context, null);
66    }
67
68    MediaRouteProvider(Context context, ProviderMetadata metadata) {
69        if (context == null) {
70            throw new IllegalArgumentException("context must not be null");
71        }
72
73        mContext = context;
74        if (metadata == null) {
75            mMetadata = new ProviderMetadata(context.getPackageName());
76        } else {
77            mMetadata = metadata;
78        }
79    }
80
81    /**
82     * Gets the context of the media route provider.
83     */
84    public final Context getContext() {
85        return mContext;
86    }
87
88    final ProviderMetadata getMetadata() {
89        return mMetadata;
90    }
91
92    /**
93     * Gets the provider's descriptor.
94     * <p>
95     * The descriptor describes the state of the media route provider and
96     * the routes that it publishes.  Watch for changes to the descriptor
97     * by registering a {@link Callback callback} with {@link #addCallback}.
98     * </p>
99     *
100     * @return The media route provider descriptor, or null if none.  This object
101     * and all of its contents should be treated as if it were immutable so that it is
102     * safe for clients to cache it.
103     */
104    public final ProviderDescriptor getDescriptor() {
105        return mDescriptor;
106    }
107
108    /**
109     * Sets the provider's descriptor.
110     * <p>
111     * Asynchronously notifies all registered {@link Callback callbacks} about the change.
112     * </p>
113     *
114     * @param descriptor The updated route provider descriptor, or null if none.
115     * This object and all of its contents should be treated as if it were immutable
116     * so that it is safe for clients to cache it.
117     */
118    public final void setDescriptor(ProviderDescriptor descriptor) {
119        MediaRouter.checkCallingThread();
120
121        if (mDescriptor != descriptor) {
122            mDescriptor = descriptor;
123            if (!mPendingDescriptorChange) {
124                mPendingDescriptorChange = true;
125                mHandler.sendEmptyMessage(MSG_DELIVER_DESCRIPTOR_CHANGED);
126            }
127        }
128    }
129
130    private void deliverDescriptorChanged() {
131        mPendingDescriptorChange = false;
132
133        if (!mCallbacks.isEmpty()) {
134            final ProviderDescriptor currentDescriptor = mDescriptor;
135            for (Callback callback : mCallbacks) {
136                callback.onDescriptorChanged(this, currentDescriptor);
137            }
138        }
139    }
140
141    /**
142     * Adds a callback to be invoked on the main thread when information about a route
143     * provider and its routes changes.
144     *
145     * @param callback The callback to add.
146     */
147    public final void addCallback(Callback callback) {
148        if (callback == null) {
149            throw new IllegalArgumentException("callback");
150        }
151
152        if (!mCallbacks.contains(callback)) {
153            mCallbacks.add(callback);
154        }
155    }
156
157    /**
158     * Removes a callback.
159     *
160     * @param callback The callback to remove.
161     */
162    public final void removeCallback(Callback callback) {
163        if (callback == null) {
164            throw new IllegalArgumentException("callback");
165        }
166
167        mCallbacks.remove(callback);
168    }
169
170    /**
171     * Called by the media router to obtain a route controller for a particular route.
172     * <p>
173     * The media router will invoke the {@link RouteController#release} method of the route
174     * controller when it is no longer needed to allow it to free its resources.
175     * </p>
176     *
177     * @param routeId The unique id of the route.
178     * @return The route controller.  Returns null if there is no such route or if the route
179     * cannot be controlled using the route controller interface.
180     */
181    public RouteController onCreateRouteController(String routeId) {
182        return null;
183    }
184
185    /**
186     * Describes immutable properties of the route provider itself.
187     */
188    static final class ProviderMetadata {
189        private final String mPackageName;
190
191        public ProviderMetadata(String packageName) {
192            if (TextUtils.isEmpty(packageName)) {
193                throw new IllegalArgumentException("packageName must not be null or empty");
194            }
195            mPackageName = packageName;
196        }
197
198        public String getPackageName() {
199            return mPackageName;
200        }
201    }
202
203    /**
204     * Describes the state of a media route provider and the routes that it publishes.
205     */
206    public static final class ProviderDescriptor {
207        private static final String KEY_ROUTES = "routes";
208
209        private final Bundle mBundle;
210        private RouteDescriptor[] mRoutes;
211
212        /**
213         * Creates a route provider descriptor.
214         */
215        public ProviderDescriptor() {
216            mBundle = new Bundle();
217        }
218
219        /**
220         * Creates a copy of another route provider descriptor.
221         */
222        public ProviderDescriptor(ProviderDescriptor other) {
223            mBundle = new Bundle(other.mBundle);
224        }
225
226        ProviderDescriptor(Bundle bundle) {
227            mBundle = bundle;
228        }
229
230        /**
231         * Gets the list of all routes that this provider has published.
232         */
233        public RouteDescriptor[] getRoutes() {
234            if (mRoutes == null) {
235                mRoutes = RouteDescriptor.fromParcelableArray(
236                        mBundle.getParcelableArray(KEY_ROUTES));
237            }
238            return mRoutes;
239        }
240
241        /**
242         * Sets the list of all routes that this provider has published.
243         */
244        public void setRoutes(RouteDescriptor[] routes) {
245            if (routes == null) {
246                throw new IllegalArgumentException("routes must not be null");
247            }
248            mRoutes = routes;
249            mBundle.putParcelableArray(KEY_ROUTES, RouteDescriptor.toParcelableArray(routes));
250        }
251
252        /**
253         * Returns true if the route provider descriptor and all of the routes that
254         * it contains have all of the required fields.
255         * <p>
256         * This verification is deep.  If the provider descriptor is known to be
257         * valid then it is not necessary to call {@link #isValid} on each of its routes.
258         * </p>
259         */
260        public boolean isValid() {
261            for (RouteDescriptor route : getRoutes()) {
262                if (route == null || !route.isValid()) {
263                    return false;
264                }
265            }
266            return true;
267        }
268
269        @Override
270        public String toString() {
271            return "RouteProviderDescriptor{" + mBundle.toString() + "}";
272        }
273
274        Bundle asBundle() {
275            return mBundle;
276        }
277
278        static ProviderDescriptor fromBundle(Bundle bundle) {
279            return bundle != null ? new ProviderDescriptor(bundle) : null;
280        }
281    }
282
283    /**
284     * Describes the properties of a route.
285     * <p>
286     * Each route is uniquely identified by an opaque id string.  This token
287     * may take any form as long as it is unique within the media route provider.
288     * </p>
289     */
290    public static final class RouteDescriptor {
291        static final RouteDescriptor[] EMPTY_ROUTE_ARRAY = new RouteDescriptor[0];
292        static final IntentFilter[] EMTPY_FILTER_ARRAY = new IntentFilter[0];
293
294        private static final String KEY_ID = "id";
295        private static final String KEY_NAME = "name";
296        private static final String KEY_STATUS = "status";
297        private static final String KEY_ICON_RESOURCE = "iconId";
298        private static final String KEY_ENABLED = "enabled";
299        private static final String KEY_CONTROL_FILTERS = "controlFilters";
300        private static final String KEY_PLAYBACK_TYPE = "playbackType";
301        private static final String KEY_PLAYBACK_STREAM = "playbackStream";
302        private static final String KEY_VOLUME = "volume";
303        private static final String KEY_VOLUME_MAX = "volumeMax";
304        private static final String KEY_VOLUME_HANDLING = "volumeHandling";
305        private static final String KEY_PRESENTATION_DISPLAY_ID = "presentationDisplayId";
306        private static final String KEY_EXTRAS = "extras";
307
308        private final Bundle mBundle;
309        private IntentFilter[] mControlFilters;
310        private Drawable mIconDrawable;
311
312        /**
313         * Creates a route descriptor.
314         *
315         * @param id The unique id of the route.
316         * @param name The user-friendly name of the route.
317         */
318        public RouteDescriptor(String id, String name) {
319            mBundle = new Bundle();
320            setId(id);
321            setName(name);
322        }
323
324        /**
325         * Creates a copy of another route descriptor.
326         */
327        public RouteDescriptor(RouteDescriptor other) {
328            mBundle = new Bundle(other.mBundle);
329        }
330
331        RouteDescriptor(Bundle bundle) {
332            mBundle = bundle;
333        }
334
335        /**
336         * Gets the unique id of the route.
337         */
338        public String getId() {
339            return mBundle.getString(KEY_ID);
340        }
341
342        /**
343         * Sets the unique id of the route.
344         */
345        public void setId(String id) {
346            mBundle.putString(KEY_ID, id);
347        }
348
349        /**
350         * Gets the user-friendly name of the route.
351         */
352        public String getName() {
353            return mBundle.getString(KEY_NAME);
354        }
355
356        /**
357         * Sets the user-friendly name of the route.
358         */
359        public void setName(String name) {
360            mBundle.putString(KEY_NAME, name);
361        }
362
363        /**
364         * Gets the user-friendly status of the route.
365         */
366        public String getStatus() {
367            return mBundle.getString(KEY_STATUS);
368        }
369
370        /**
371         * Sets the user-friendly status of the route.
372         */
373        public void setStatus(String status) {
374            mBundle.putString(KEY_STATUS, status);
375        }
376
377        /**
378         * Gets a drawable to display as the route's icon.
379         * <p>
380         * Because drawables cannot be transferred to other processes, this method may
381         * only be used by media route providers that reside in the same process
382         * as the application.  When implementing a media route provider service, use
383         * {@link #getIconResource} instead.
384         * </p>
385         */
386        public Drawable getIconDrawable() {
387            return mIconDrawable;
388        }
389
390        /**
391         * Sets a drawable to display as the route's icon.
392         * <p>
393         * Because drawables cannot be transferred to other processes, this method may
394         * only be used by media route providers that reside in the same process
395         * as the application.  When implementing a media route provider service, use
396         * {@link #setIconResource} instead.
397         * </p>
398         */
399        public void setIconDrawable(Drawable drawable) {
400            mIconDrawable = drawable;
401        }
402
403        /**
404         * Gets the id of a drawable resource to display as the route's icon.
405         * <p>
406         * The specified drawable resource id will be loaded from the media route
407         * provider's package.
408         * </p>
409         */
410        public int getIconResource() {
411            return mBundle.getInt(KEY_ICON_RESOURCE);
412        }
413
414        /**
415         * Sets the id of a drawable resource to display as the route's icon.
416         * <p>
417         * The specified drawable resource id will be loaded from the media route
418         * provider's package.
419         * </p>
420         */
421        public void setIconResource(int id) {
422            mBundle.putInt(KEY_ICON_RESOURCE, id);
423        }
424
425        /**
426         * Gets whether the route is enabled.
427         */
428        public boolean isEnabled() {
429            return mBundle.getBoolean(KEY_ENABLED, true);
430        }
431
432        /**
433         * Sets whether the route is enabled.
434         */
435        public void setEnabled(boolean enabled) {
436            mBundle.putBoolean(KEY_ENABLED, enabled);
437        }
438
439        /**
440         * Gets the route's {@link MediaControlIntent media control intent} filters.
441         */
442        public IntentFilter[] getControlFilters() {
443            if (mControlFilters == null) {
444                Parcelable[] filters = mBundle.getParcelableArray(KEY_CONTROL_FILTERS);
445                if (filters instanceof IntentFilter[]) {
446                    mControlFilters = (IntentFilter[])filters;
447                } else if (filters != null && filters.length > 0) {
448                    mControlFilters = new IntentFilter[filters.length];
449                    System.arraycopy(filters, 0, mControlFilters, 0, filters.length);
450                } else {
451                    mControlFilters = EMTPY_FILTER_ARRAY;
452                }
453            }
454            return mControlFilters;
455        }
456
457        /**
458         * Sets the route's {@link MediaControlIntent media control intent} filters.
459         */
460        public void setControlFilters(IntentFilter[] controlFilters) {
461            if (controlFilters == null) {
462                throw new IllegalArgumentException("controlFilters must not be null");
463            }
464            mControlFilters = controlFilters;
465            mBundle.putParcelableArray(KEY_CONTROL_FILTERS, controlFilters);
466        }
467
468        /**
469         * Gets the route's playback type.
470         */
471        public int getPlaybackType() {
472            return mBundle.getInt(KEY_PLAYBACK_TYPE, MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE);
473        }
474
475        /**
476         * Sets the route's playback type.
477         */
478        public void setPlaybackType(int playbackType) {
479            mBundle.putInt(KEY_PLAYBACK_TYPE, playbackType);
480        }
481
482        /**
483         * Gets the route's playback stream.
484         */
485        public int getPlaybackStream() {
486            return mBundle.getInt(KEY_PLAYBACK_STREAM, -1);
487        }
488
489        /**
490         * Sets the route's playback stream.
491         */
492        public void setPlaybackStream(int playbackStream) {
493            mBundle.putInt(KEY_PLAYBACK_STREAM, playbackStream);
494        }
495
496        /**
497         * Gets the route's current volume, or 0 if unknown.
498         */
499        public int getVolume() {
500            return mBundle.getInt(KEY_VOLUME);
501        }
502
503        /**
504         * Sets the route's current volume, or 0 if unknown.
505         */
506        public void setVolume(int volume) {
507            mBundle.putInt(KEY_VOLUME, volume);
508        }
509
510        /**
511         * Gets the route's maximum volume, or 0 if unknown.
512         */
513        public int getVolumeMax() {
514            return mBundle.getInt(KEY_VOLUME_MAX);
515        }
516
517        /**
518         * Sets the route's maximum volume, or 0 if unknown.
519         */
520        public void setVolumeMax(int volumeMax) {
521            mBundle.putInt(KEY_VOLUME_MAX, volumeMax);
522        }
523
524        /**
525         * Gets the route's volume handling.
526         */
527        public int getVolumeHandling() {
528            return mBundle.getInt(KEY_VOLUME_HANDLING,
529                    MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED);
530        }
531
532        /**
533         * Sets the route's volume handling.
534         */
535        public void setVolumeHandling(int volumeHandling) {
536            mBundle.putInt(KEY_VOLUME_HANDLING, volumeHandling);
537        }
538
539        /**
540         * Gets the route's presentation display id, or -1 if none.
541         */
542        public int getPresentationDisplayId() {
543            return mBundle.getInt(KEY_PRESENTATION_DISPLAY_ID, -1);
544        }
545
546        /**
547         * Sets the route's presentation display id, or -1 if none.
548         */
549        public void setPresentationDisplayId(int presentationDisplayId) {
550            mBundle.putInt(KEY_PRESENTATION_DISPLAY_ID, presentationDisplayId);
551        }
552
553        /**
554         * Gets a bundle of extras for this route descriptor.
555         * The extras will be ignored by the media router but they may be used
556         * by applications.
557         */
558        public Bundle getExtras() {
559            return mBundle.getBundle(KEY_EXTRAS);
560        }
561
562        /**
563         * Sets a bundle of extras for this route descriptor.
564         * The extras will be ignored by the media router but they may be used
565         * by applications.
566         */
567        public void setExtras(Bundle extras) {
568            mBundle.putBundle(KEY_EXTRAS, extras);
569        }
570
571        /**
572         * Returns true if the route descriptor has all of the required fields.
573         */
574        public boolean isValid() {
575            if (TextUtils.isEmpty(getId())
576                    || TextUtils.isEmpty(getName())) {
577                return false;
578            }
579            for (IntentFilter filter : getControlFilters()) {
580                if (filter == null) {
581                    return false;
582                }
583            }
584            return true;
585        }
586
587        @Override
588        public String toString() {
589            return "RouteDescriptor{" + mBundle.toString() + "}";
590        }
591
592        Bundle asBundle() {
593            return mBundle;
594        }
595
596        static Parcelable[] toParcelableArray(RouteDescriptor[] descriptors) {
597            if (descriptors != null && descriptors.length > 0) {
598                Parcelable[] bundles = new Parcelable[descriptors.length];
599                for (int i = 0; i < descriptors.length; i++) {
600                    bundles[i] = descriptors[i].asBundle();
601                }
602                return bundles;
603            }
604            return null;
605        }
606
607        static RouteDescriptor[] fromParcelableArray(Parcelable[] bundles) {
608            if (bundles != null && bundles.length > 0) {
609                RouteDescriptor[] descriptors = new RouteDescriptor[bundles.length];
610                for (int i = 0; i < bundles.length; i++) {
611                    descriptors[i] = new RouteDescriptor((Bundle)bundles[i]);
612                }
613                return descriptors;
614            }
615            return EMPTY_ROUTE_ARRAY;
616        }
617    }
618
619    /**
620     * Provides control over a particular route.
621     * <p>
622     * The media router obtains a route controller for a route whenever it needs
623     * to control a route.  When a route is selected, the media router invokes
624     * the {@link #select} method of its route controller.  While selected,
625     * the media router may call other methods of the route controller to
626     * request that it perform certain actions to the route.  When a route is
627     * unselected, the media router invokes the {@link #unselect} method of its
628     * route controller.  When the media route no longer needs the route controller
629     * it will invoke the {@link #release} method to allow the route controller
630     * to free its resources.
631     * </p><p>
632     * There may be multiple route controllers simultaneously active for the
633     * same route.  Each route controller will be released separately.
634     * </p><p>
635     * All operations on the route controller are asynchronous and
636     * results are communicated via callbacks.
637     * </p>
638     */
639    public static abstract class RouteController {
640        /**
641         * Releases the route controller, allowing it to free its resources.
642         */
643        public void release() {
644        }
645
646        /**
647         * Selects the route.
648         */
649        public void select() {
650        }
651
652        /**
653         * Unselects the route.
654         */
655        public void unselect() {
656        }
657
658        /**
659         * Requests to set the volume of the route.
660         *
661         * @param volume The new volume value between 0 and {@link RouteDescriptor#getVolumeMax}.
662         */
663        public void setVolume(int volume) {
664        }
665
666        /**
667         * Requests an incremental volume update for the route.
668         *
669         * @param delta The delta to add to the current volume.
670         */
671        public void updateVolume(int delta) {
672        }
673
674        /**
675         * Sends a {@link MediaControlIntent media control} request to be performed
676         * asynchronously by the route's destination.
677         *
678         * @param intent A {@link MediaControlIntent media control intent}.
679         * @param callback A {@link ControlRequestCallback} to invoke with the result
680         * of the request, or null if no result is required.
681         * @return True if the controller intends to handle the request and will
682         * invoke the callback when finished.  False if the contorller will not
683         * handle the request and will not invoke the callback.
684         *
685         * @see MediaControlIntent
686         */
687        public boolean sendControlRequest(Intent intent, ControlRequestCallback callback) {
688            return false;
689        }
690    }
691
692    /**
693     * Callback which is invoked when route information becomes available or changes.
694     */
695    public static abstract class Callback {
696        /**
697         * Called when information about a route provider and its routes changes.
698         *
699         * @param provider The media route provider that changed.
700         * and all of its contents should be treated as if it were immutable so that it is
701         * safe for clients to cache it.
702         */
703        public void onDescriptorChanged(MediaRouteProvider provider,
704                ProviderDescriptor descriptor) {
705        }
706    }
707
708    private final class ProviderHandler extends Handler {
709        @Override
710        public void handleMessage(Message msg) {
711            switch (msg.what) {
712                case MSG_DELIVER_DESCRIPTOR_CHANGED:
713                    deliverDescriptorChanged();
714                    break;
715            }
716        }
717    }
718}
719