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 */
16package android.support.v7.media;
17
18import android.content.IntentFilter;
19import android.content.IntentSender;
20import android.net.Uri;
21import android.os.Bundle;
22import android.text.TextUtils;
23
24import java.util.ArrayList;
25import java.util.Arrays;
26import java.util.Collection;
27import java.util.Collections;
28import java.util.List;
29
30/**
31 * Describes the properties of a route.
32 * <p>
33 * Each route is uniquely identified by an opaque id string.  This token
34 * may take any form as long as it is unique within the media route provider.
35 * </p><p>
36 * This object is immutable once created using a {@link Builder} instance.
37 * </p>
38 */
39public final class MediaRouteDescriptor {
40    private static final String KEY_ID = "id";
41    private static final String KEY_GROUP_MEMBER_IDS = "groupMemberIds";
42    private static final String KEY_NAME = "name";
43    private static final String KEY_DESCRIPTION = "status";
44    private static final String KEY_ICON_URI = "iconUri";
45    private static final String KEY_ENABLED = "enabled";
46    private static final String KEY_CONNECTING = "connecting";
47    private static final String KEY_CONNECTION_STATE = "connectionState";
48    private static final String KEY_CONTROL_FILTERS = "controlFilters";
49    private static final String KEY_PLAYBACK_TYPE = "playbackType";
50    private static final String KEY_PLAYBACK_STREAM = "playbackStream";
51    private static final String KEY_DEVICE_TYPE = "deviceType";
52    private static final String KEY_VOLUME = "volume";
53    private static final String KEY_VOLUME_MAX = "volumeMax";
54    private static final String KEY_VOLUME_HANDLING = "volumeHandling";
55    private static final String KEY_PRESENTATION_DISPLAY_ID = "presentationDisplayId";
56    private static final String KEY_EXTRAS = "extras";
57    private static final String KEY_CAN_DISCONNECT = "canDisconnect";
58    private static final String KEY_SETTINGS_INTENT = "settingsIntent";
59
60    private final Bundle mBundle;
61    private List<IntentFilter> mControlFilters;
62
63    private MediaRouteDescriptor(Bundle bundle, List<IntentFilter> controlFilters) {
64        mBundle = bundle;
65        mControlFilters = controlFilters;
66    }
67
68    /**
69     * Gets the unique id of the route.
70     * <p>
71     * The route id associated with a route descriptor functions as a stable
72     * identifier for the route and must be unique among all routes offered
73     * by the provider.
74     * </p>
75     */
76    public String getId() {
77        return mBundle.getString(KEY_ID);
78    }
79
80    /**
81     * Gets the group member ids of the route.
82     * <p>
83     * A route descriptor that has one or more group member route ids
84     * represents a route group. A member route may belong to another group.
85     * </p>
86     * @hide
87     */
88    public List<String> getGroupMemberIds() {
89        return mBundle.getStringArrayList(KEY_GROUP_MEMBER_IDS);
90    }
91
92    /**
93     * Gets the user-visible name of the route.
94     * <p>
95     * The route name identifies the destination represented by the route.
96     * It may be a user-supplied name, an alias, or device serial number.
97     * </p>
98     */
99    public String getName() {
100        return mBundle.getString(KEY_NAME);
101    }
102
103    /**
104     * Gets the user-visible description of the route.
105     * <p>
106     * The route description describes the kind of destination represented by the route.
107     * It may be a user-supplied string, a model number or brand of device.
108     * </p>
109     */
110    public String getDescription() {
111        return mBundle.getString(KEY_DESCRIPTION);
112    }
113
114    /**
115     * Gets the URI of the icon representing this route.
116     * <p>
117     * This icon will be used in picker UIs if available.
118     * </p>
119     */
120    public Uri getIconUri() {
121        String iconUri = mBundle.getString(KEY_ICON_URI);
122        return iconUri == null ? null : Uri.parse(iconUri);
123    }
124
125    /**
126     * Gets whether the route is enabled.
127     */
128    public boolean isEnabled() {
129        return mBundle.getBoolean(KEY_ENABLED, true);
130    }
131
132    /**
133     * Gets whether the route is connecting.
134     * @deprecated Use {@link #getConnectionState} instead
135     */
136    @Deprecated
137    public boolean isConnecting() {
138        return mBundle.getBoolean(KEY_CONNECTING, false);
139    }
140
141    /**
142     * Gets the connection state of the route.
143     *
144     * @return The connection state of this route:
145     * {@link MediaRouter.RouteInfo#CONNECTION_STATE_DISCONNECTED},
146     * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTING}, or
147     * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTED}.
148     */
149    public int getConnectionState() {
150        return mBundle.getInt(KEY_CONNECTION_STATE,
151                MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED);
152    }
153
154    /**
155     * Gets whether the route can be disconnected without stopping playback.
156     * <p>
157     * The route can normally be disconnected without stopping playback when
158     * the destination device on the route is connected to two or more source
159     * devices. The route provider should update the route immediately when the
160     * number of connected devices changes.
161     * </p><p>
162     * To specify that the route should disconnect without stopping use
163     * {@link MediaRouter#unselect(int)} with
164     * {@link MediaRouter#UNSELECT_REASON_DISCONNECTED}.
165     * </p>
166     */
167    public boolean canDisconnectAndKeepPlaying() {
168        return mBundle.getBoolean(KEY_CAN_DISCONNECT, false);
169    }
170
171    /**
172     * Gets an {@link IntentSender} for starting a settings activity for this
173     * route. The activity may have specific route settings or general settings
174     * for the connected device or route provider.
175     *
176     * @return An {@link IntentSender} to start a settings activity.
177     */
178    public IntentSender getSettingsActivity() {
179        return mBundle.getParcelable(KEY_SETTINGS_INTENT);
180    }
181
182    /**
183     * Gets the route's {@link MediaControlIntent media control intent} filters.
184     */
185    public List<IntentFilter> getControlFilters() {
186        ensureControlFilters();
187        return mControlFilters;
188    }
189
190    private void ensureControlFilters() {
191        if (mControlFilters == null) {
192            mControlFilters = mBundle.<IntentFilter>getParcelableArrayList(KEY_CONTROL_FILTERS);
193            if (mControlFilters == null) {
194                mControlFilters = Collections.<IntentFilter>emptyList();
195            }
196        }
197    }
198
199    /**
200     * Gets the type of playback associated with this route.
201     *
202     * @return The type of playback associated with this route:
203     * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_LOCAL} or
204     * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_REMOTE}.
205     */
206    public int getPlaybackType() {
207        return mBundle.getInt(KEY_PLAYBACK_TYPE, MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE);
208    }
209
210    /**
211     * Gets the route's playback stream.
212     */
213    public int getPlaybackStream() {
214        return mBundle.getInt(KEY_PLAYBACK_STREAM, -1);
215    }
216
217    /**
218     * Gets the type of the receiver device associated with this route.
219     *
220     * @return The type of the receiver device associated with this route:
221     * {@link MediaRouter.RouteInfo#DEVICE_TYPE_TV} or
222     * {@link MediaRouter.RouteInfo#DEVICE_TYPE_SPEAKER}.
223     */
224    public int getDeviceType() {
225        return mBundle.getInt(KEY_DEVICE_TYPE);
226    }
227
228    /**
229     * Gets the route's current volume, or 0 if unknown.
230     */
231    public int getVolume() {
232        return mBundle.getInt(KEY_VOLUME);
233    }
234
235    /**
236     * Gets the route's maximum volume, or 0 if unknown.
237     */
238    public int getVolumeMax() {
239        return mBundle.getInt(KEY_VOLUME_MAX);
240    }
241
242    /**
243     * Gets information about how volume is handled on the route.
244     *
245     * @return How volume is handled on the route:
246     * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_FIXED} or
247     * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_VARIABLE}.
248     */
249    public int getVolumeHandling() {
250        return mBundle.getInt(KEY_VOLUME_HANDLING,
251                MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED);
252    }
253
254    /**
255     * Gets the route's presentation display id, or -1 if none.
256     */
257    public int getPresentationDisplayId() {
258        return mBundle.getInt(
259                KEY_PRESENTATION_DISPLAY_ID, MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE);
260    }
261
262    /**
263     * Gets a bundle of extras for this route descriptor.
264     * The extras will be ignored by the media router but they may be used
265     * by applications.
266     */
267    public Bundle getExtras() {
268        return mBundle.getBundle(KEY_EXTRAS);
269    }
270
271    /**
272     * Returns true if the route descriptor has all of the required fields.
273     */
274    public boolean isValid() {
275        ensureControlFilters();
276        if (TextUtils.isEmpty(getId())
277                || TextUtils.isEmpty(getName())
278                || mControlFilters.contains(null)) {
279            return false;
280        }
281        return true;
282    }
283
284    @Override
285    public String toString() {
286        StringBuilder result = new StringBuilder();
287        result.append("MediaRouteDescriptor{ ");
288        result.append("id=").append(getId());
289        result.append(", groupMemberIds=").append(getGroupMemberIds());
290        result.append(", name=").append(getName());
291        result.append(", description=").append(getDescription());
292        result.append(", iconUri=").append(getIconUri());
293        result.append(", isEnabled=").append(isEnabled());
294        result.append(", isConnecting=").append(isConnecting());
295        result.append(", connectionState=").append(getConnectionState());
296        result.append(", controlFilters=").append(Arrays.toString(getControlFilters().toArray()));
297        result.append(", playbackType=").append(getPlaybackType());
298        result.append(", playbackStream=").append(getPlaybackStream());
299        result.append(", deviceType=").append(getDeviceType());
300        result.append(", volume=").append(getVolume());
301        result.append(", volumeMax=").append(getVolumeMax());
302        result.append(", volumeHandling=").append(getVolumeHandling());
303        result.append(", presentationDisplayId=").append(getPresentationDisplayId());
304        result.append(", extras=").append(getExtras());
305        result.append(", isValid=").append(isValid());
306        result.append(" }");
307        return result.toString();
308    }
309
310    /**
311     * Converts this object to a bundle for serialization.
312     *
313     * @return The contents of the object represented as a bundle.
314     */
315    public Bundle asBundle() {
316        return mBundle;
317    }
318
319    /**
320     * Creates an instance from a bundle.
321     *
322     * @param bundle The bundle, or null if none.
323     * @return The new instance, or null if the bundle was null.
324     */
325    public static MediaRouteDescriptor fromBundle(Bundle bundle) {
326        return bundle != null ? new MediaRouteDescriptor(bundle, null) : null;
327    }
328
329    /**
330     * Builder for {@link MediaRouteDescriptor media route descriptors}.
331     */
332    public static final class Builder {
333        private final Bundle mBundle;
334        private ArrayList<String> mGroupMemberIds;
335        private ArrayList<IntentFilter> mControlFilters;
336
337        /**
338         * Creates a media route descriptor builder.
339         *
340         * @param id The unique id of the route.
341         * @param name The user-visible name of the route.
342         */
343        public Builder(String id, String name) {
344            mBundle = new Bundle();
345            setId(id);
346            setName(name);
347        }
348
349        /**
350         * Creates a media route descriptor builder whose initial contents are
351         * copied from an existing descriptor.
352         */
353        public Builder(MediaRouteDescriptor descriptor) {
354            if (descriptor == null) {
355                throw new IllegalArgumentException("descriptor must not be null");
356            }
357
358            mBundle = new Bundle(descriptor.mBundle);
359
360            descriptor.ensureControlFilters();
361            if (!descriptor.mControlFilters.isEmpty()) {
362                mControlFilters = new ArrayList<IntentFilter>(descriptor.mControlFilters);
363            }
364        }
365
366        /**
367         * Sets the unique id of the route.
368         * <p>
369         * The route id associated with a route descriptor functions as a stable
370         * identifier for the route and must be unique among all routes offered
371         * by the provider.
372         * </p>
373         */
374        public Builder setId(String id) {
375            mBundle.putString(KEY_ID, id);
376            return this;
377        }
378
379        /**
380         * Adds a group member id of the route.
381         * <p>
382         * A route descriptor that has one or more group member route ids
383         * represents a route group. A member route may belong to another group.
384         * </p>
385         * @hide
386         */
387        public Builder addGroupMemberId(String groupMemberId) {
388            if (TextUtils.isEmpty(groupMemberId)) {
389                throw new IllegalArgumentException("groupMemberId must not be empty");
390            }
391
392            if (mGroupMemberIds == null) {
393                mGroupMemberIds = new ArrayList<>();
394            }
395            if (!mGroupMemberIds.contains(groupMemberId)) {
396                mGroupMemberIds.add(groupMemberId);
397            }
398            return this;
399        }
400
401        /**
402         * Adds a list of group member ids of the route.
403         * <p>
404         * A route descriptor that has one or more group member route ids
405         * represents a route group. A member route may belong to another group.
406         * </p>
407         * @hide
408         */
409        public Builder addGroupMemberIds(Collection<String> groupMemberIds) {
410            if (groupMemberIds == null) {
411                throw new IllegalArgumentException("groupMemberIds must not be null");
412            }
413
414            if (!groupMemberIds.isEmpty()) {
415                for (String groupMemberId : groupMemberIds) {
416                    addGroupMemberId(groupMemberId);
417                }
418            }
419            return this;
420        }
421
422        /**
423         * Sets the user-visible name of the route.
424         * <p>
425         * The route name identifies the destination represented by the route.
426         * It may be a user-supplied name, an alias, or device serial number.
427         * </p>
428         */
429        public Builder setName(String name) {
430            mBundle.putString(KEY_NAME, name);
431            return this;
432        }
433
434        /**
435         * Sets the user-visible description of the route.
436         * <p>
437         * The route description describes the kind of destination represented by the route.
438         * It may be a user-supplied string, a model number or brand of device.
439         * </p>
440         */
441        public Builder setDescription(String description) {
442            mBundle.putString(KEY_DESCRIPTION, description);
443            return this;
444        }
445
446        /**
447         * Sets the URI of the icon representing this route.
448         * <p>
449         * This icon will be used in picker UIs if available.
450         * </p><p>
451         * The URI must be one of the following formats:
452         * <ul>
453         * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
454         * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
455         * </li>
456         * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
457         * </ul>
458         * </p>
459         */
460        public Builder setIconUri(Uri iconUri) {
461            if (iconUri == null) {
462                throw new IllegalArgumentException("iconUri must not be null");
463            }
464            mBundle.putString(KEY_ICON_URI, iconUri.toString());
465            return this;
466        }
467
468        /**
469         * Sets whether the route is enabled.
470         * <p>
471         * Disabled routes represent routes that a route provider knows about, such as paired
472         * Wifi Display receivers, but that are not currently available for use.
473         * </p>
474         */
475        public Builder setEnabled(boolean enabled) {
476            mBundle.putBoolean(KEY_ENABLED, enabled);
477            return this;
478        }
479
480        /**
481         * Sets whether the route is in the process of connecting and is not yet
482         * ready for use.
483         * @deprecated Use {@link #setConnectionState} instead.
484         */
485        @Deprecated
486        public Builder setConnecting(boolean connecting) {
487            mBundle.putBoolean(KEY_CONNECTING, connecting);
488            return this;
489        }
490
491        /**
492         * Sets the route's connection state.
493         *
494         * @param connectionState The connection state of the route:
495         * {@link MediaRouter.RouteInfo#CONNECTION_STATE_DISCONNECTED},
496         * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTING}, or
497         * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTED}.
498         */
499        public Builder setConnectionState(int connectionState) {
500            mBundle.putInt(KEY_CONNECTION_STATE, connectionState);
501            return this;
502        }
503
504        /**
505         * Sets whether the route can be disconnected without stopping playback.
506         */
507        public Builder setCanDisconnect(boolean canDisconnect) {
508            mBundle.putBoolean(KEY_CAN_DISCONNECT, canDisconnect);
509            return this;
510        }
511
512        /**
513         * Sets an intent sender for launching the settings activity for this
514         * route.
515         */
516        public Builder setSettingsActivity(IntentSender is) {
517            mBundle.putParcelable(KEY_SETTINGS_INTENT, is);
518            return this;
519        }
520
521        /**
522         * Adds a {@link MediaControlIntent media control intent} filter for the route.
523         */
524        public Builder addControlFilter(IntentFilter filter) {
525            if (filter == null) {
526                throw new IllegalArgumentException("filter must not be null");
527            }
528
529            if (mControlFilters == null) {
530                mControlFilters = new ArrayList<IntentFilter>();
531            }
532            if (!mControlFilters.contains(filter)) {
533                mControlFilters.add(filter);
534            }
535            return this;
536        }
537
538        /**
539         * Adds a list of {@link MediaControlIntent media control intent} filters for the route.
540         */
541        public Builder addControlFilters(Collection<IntentFilter> filters) {
542            if (filters == null) {
543                throw new IllegalArgumentException("filters must not be null");
544            }
545
546            if (!filters.isEmpty()) {
547                for (IntentFilter filter : filters) {
548                    addControlFilter(filter);
549                }
550            }
551            return this;
552        }
553
554        /**
555         * Sets the route's playback type.
556         *
557         * @param playbackType The playback type of the route:
558         * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_LOCAL} or
559         * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_REMOTE}.
560         */
561        public Builder setPlaybackType(int playbackType) {
562            mBundle.putInt(KEY_PLAYBACK_TYPE, playbackType);
563            return this;
564        }
565
566        /**
567         * Sets the route's playback stream.
568         */
569        public Builder setPlaybackStream(int playbackStream) {
570            mBundle.putInt(KEY_PLAYBACK_STREAM, playbackStream);
571            return this;
572        }
573
574        /**
575         * Sets the route's receiver device type.
576         *
577         * @param deviceType The receive device type of the route:
578         * {@link MediaRouter.RouteInfo#DEVICE_TYPE_TV} or
579         * {@link MediaRouter.RouteInfo#DEVICE_TYPE_SPEAKER}.
580         */
581        public Builder setDeviceType(int deviceType) {
582            mBundle.putInt(KEY_DEVICE_TYPE, deviceType);
583            return this;
584        }
585
586        /**
587         * Sets the route's current volume, or 0 if unknown.
588         */
589        public Builder setVolume(int volume) {
590            mBundle.putInt(KEY_VOLUME, volume);
591            return this;
592        }
593
594        /**
595         * Sets the route's maximum volume, or 0 if unknown.
596         */
597        public Builder setVolumeMax(int volumeMax) {
598            mBundle.putInt(KEY_VOLUME_MAX, volumeMax);
599            return this;
600        }
601
602        /**
603         * Sets the route's volume handling.
604         *
605         * @param volumeHandling how volume is handled on the route:
606         * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_FIXED} or
607         * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_VARIABLE}.
608         */
609        public Builder setVolumeHandling(int volumeHandling) {
610            mBundle.putInt(KEY_VOLUME_HANDLING, volumeHandling);
611            return this;
612        }
613
614        /**
615         * Sets the route's presentation display id, or -1 if none.
616         */
617        public Builder setPresentationDisplayId(int presentationDisplayId) {
618            mBundle.putInt(KEY_PRESENTATION_DISPLAY_ID, presentationDisplayId);
619            return this;
620        }
621
622        /**
623         * Sets a bundle of extras for this route descriptor.
624         * The extras will be ignored by the media router but they may be used
625         * by applications.
626         */
627        public Builder setExtras(Bundle extras) {
628            mBundle.putBundle(KEY_EXTRAS, extras);
629            return this;
630        }
631
632        /**
633         * Builds the {@link MediaRouteDescriptor media route descriptor}.
634         */
635        public MediaRouteDescriptor build() {
636            if (mControlFilters != null) {
637                mBundle.putParcelableArrayList(KEY_CONTROL_FILTERS, mControlFilters);
638            }
639            if (mGroupMemberIds != null) {
640                mBundle.putStringArrayList(KEY_GROUP_MEMBER_IDS, mGroupMemberIds);
641            }
642            return new MediaRouteDescriptor(mBundle, mControlFilters);
643        }
644    }
645}