SystemMediaRouteProvider.java revision 55b361aea868e53e848bc45af3a55ae43e7871c3
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.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.content.res.Resources;
24import android.media.AudioManager;
25import android.os.Build;
26import android.support.v7.mediarouter.R;
27import android.view.Display;
28
29import java.util.ArrayList;
30import java.util.List;
31
32/**
33 * Provides routes for built-in system destinations such as the local display
34 * and speaker.  On Jellybean and newer platform releases, queries the framework
35 * MediaRouter for framework-provided routes and registers non-framework-provided
36 * routes as user routes.
37 */
38abstract class SystemMediaRouteProvider extends MediaRouteProvider {
39    private static final String TAG = "SystemMediaRouteProvider";
40
41    public static final String PACKAGE_NAME = "android";
42    public static final String DEFAULT_ROUTE_ID = "DEFAULT_ROUTE";
43
44    protected SystemMediaRouteProvider(Context context) {
45        super(context, new ProviderMetadata(PACKAGE_NAME));
46    }
47
48    public static SystemMediaRouteProvider obtain(Context context, SyncCallback syncCallback) {
49        if (Build.VERSION.SDK_INT >= 18) {
50            return new JellybeanMr2Impl(context, syncCallback);
51        }
52        if (Build.VERSION.SDK_INT >= 17) {
53            return new JellybeanMr1Impl(context, syncCallback);
54        }
55        if (Build.VERSION.SDK_INT >= 16) {
56            return new JellybeanImpl(context, syncCallback);
57        }
58        return new LegacyImpl(context);
59    }
60
61    /**
62     * Called by the media router when a route is added to synchronize state with
63     * the framework media router.
64     */
65    public void onSyncRouteAdded(MediaRouter.RouteInfo route) {
66    }
67
68    /**
69     * Called by the media router when a route is removed to synchronize state with
70     * the framework media router.
71     */
72    public void onSyncRouteRemoved(MediaRouter.RouteInfo route) {
73    }
74
75    /**
76     * Called by the media router when a route is changed to synchronize state with
77     * the framework media router.
78     */
79    public void onSyncRouteChanged(MediaRouter.RouteInfo route) {
80    }
81
82    /**
83     * Called by the media router when a route is selected to synchronize state with
84     * the framework media router.
85     */
86    public void onSyncRouteSelected(MediaRouter.RouteInfo route) {
87    }
88
89    /**
90     * Callbacks into the media router to synchronize state with the framework media router.
91     */
92    public interface SyncCallback {
93        public MediaRouter.RouteInfo getSystemRouteByDescriptorId(String id);
94    }
95
96    /**
97     * Legacy implementation for platform versions prior to Jellybean.
98     */
99    static class LegacyImpl extends SystemMediaRouteProvider {
100        private static final int PLAYBACK_STREAM = AudioManager.STREAM_MUSIC;
101
102        private static final ArrayList<IntentFilter> CONTROL_FILTERS;
103        static {
104            IntentFilter f = new IntentFilter();
105            f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
106            f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
107
108            CONTROL_FILTERS = new ArrayList<IntentFilter>();
109            CONTROL_FILTERS.add(f);
110        }
111
112        private final AudioManager mAudioManager;
113        private final VolumeChangeReceiver mVolumeChangeReceiver;
114        private int mLastReportedVolume = -1;
115
116        public LegacyImpl(Context context) {
117            super(context);
118            mAudioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
119            mVolumeChangeReceiver = new VolumeChangeReceiver();
120
121            context.registerReceiver(mVolumeChangeReceiver,
122                    new IntentFilter(VolumeChangeReceiver.VOLUME_CHANGED_ACTION));
123            publishRoutes();
124        }
125
126        private void publishRoutes() {
127            Resources r = getContext().getResources();
128            int maxVolume = mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM);
129            mLastReportedVolume = mAudioManager.getStreamVolume(PLAYBACK_STREAM);
130            MediaRouteDescriptor defaultRoute = new MediaRouteDescriptor.Builder(
131                    DEFAULT_ROUTE_ID, r.getString(R.string.system_route_name))
132                    .addControlFilters(CONTROL_FILTERS)
133                    .setPlaybackStream(PLAYBACK_STREAM)
134                    .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL)
135                    .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
136                    .setVolumeMax(maxVolume)
137                    .setVolume(mLastReportedVolume)
138                    .build();
139
140            MediaRouteProviderDescriptor providerDescriptor =
141                    new MediaRouteProviderDescriptor.Builder()
142                    .addDiscoverableControlFilters(CONTROL_FILTERS)
143                    .addRoute(defaultRoute)
144                    .build();
145            setDescriptor(providerDescriptor);
146        }
147
148        @Override
149        public RouteController onCreateRouteController(String routeId) {
150            if (routeId.equals(DEFAULT_ROUTE_ID)) {
151                return new DefaultRouteController();
152            }
153            return null;
154        }
155
156        final class DefaultRouteController extends RouteController {
157            @Override
158            public void onSetVolume(int volume) {
159                mAudioManager.setStreamVolume(PLAYBACK_STREAM, volume, 0);
160                publishRoutes();
161            }
162
163            @Override
164            public void onUpdateVolume(int delta) {
165                int volume = mAudioManager.getStreamVolume(PLAYBACK_STREAM);
166                int maxVolume = mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM);
167                int newVolume = Math.min(maxVolume, Math.max(0, volume + delta));
168                if (newVolume != volume) {
169                    mAudioManager.setStreamVolume(PLAYBACK_STREAM, volume, 0);
170                }
171                publishRoutes();
172            }
173        }
174
175        final class VolumeChangeReceiver extends BroadcastReceiver {
176            // These constants come from AudioManager.
177            public static final String VOLUME_CHANGED_ACTION =
178                    "android.media.VOLUME_CHANGED_ACTION";
179            public static final String EXTRA_VOLUME_STREAM_TYPE =
180                    "android.media.EXTRA_VOLUME_STREAM_TYPE";
181            public static final String EXTRA_VOLUME_STREAM_VALUE =
182                    "android.media.EXTRA_VOLUME_STREAM_VALUE";
183
184            @Override
185            public void onReceive(Context context, Intent intent) {
186                if (intent.getAction().equals(VOLUME_CHANGED_ACTION)) {
187                    final int streamType = intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, -1);
188                    if (streamType == PLAYBACK_STREAM) {
189                        final int volume = intent.getIntExtra(EXTRA_VOLUME_STREAM_VALUE, -1);
190                        if (volume >= 0 && volume != mLastReportedVolume) {
191                            publishRoutes();
192                        }
193                    }
194                }
195            }
196        }
197    }
198
199    /**
200     * Jellybean implementation.
201     */
202    static class JellybeanImpl extends SystemMediaRouteProvider
203            implements MediaRouterJellybean.Callback, MediaRouterJellybean.VolumeCallback {
204        private static final ArrayList<IntentFilter> LIVE_AUDIO_CONTROL_FILTERS;
205        static {
206            IntentFilter f = new IntentFilter();
207            f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
208
209            LIVE_AUDIO_CONTROL_FILTERS = new ArrayList<IntentFilter>();
210            LIVE_AUDIO_CONTROL_FILTERS.add(f);
211        }
212
213        private static final ArrayList<IntentFilter> LIVE_VIDEO_CONTROL_FILTERS;
214        static {
215            IntentFilter f = new IntentFilter();
216            f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
217
218            LIVE_VIDEO_CONTROL_FILTERS = new ArrayList<IntentFilter>();
219            LIVE_VIDEO_CONTROL_FILTERS.add(f);
220        }
221
222        private final SyncCallback mSyncCallback;
223
224        protected final Object mRouterObj;
225        protected final Object mCallbackObj;
226        protected final Object mVolumeCallbackObj;
227        protected final Object mUserRouteCategoryObj;
228        protected int mRouteTypes;
229        protected boolean mActiveScan;
230        protected boolean mCallbackRegistered;
231
232        // Maintains an association from framework routes to support library routes.
233        // Note that we cannot use the tag field for this because an application may
234        // have published its own user routes to the framework media router and already
235        // used the tag for its own purposes.
236        protected final ArrayList<SystemRouteRecord> mSystemRouteRecords =
237                new ArrayList<SystemRouteRecord>();
238
239        // Maintains an association from support library routes to framework routes.
240        protected final ArrayList<UserRouteRecord> mUserRouteRecords =
241                new ArrayList<UserRouteRecord>();
242
243        private MediaRouterJellybean.SelectRouteWorkaround mSelectRouteWorkaround;
244        private MediaRouterJellybean.GetDefaultRouteWorkaround mGetDefaultRouteWorkaround;
245
246        public JellybeanImpl(Context context, SyncCallback syncCallback) {
247            super(context);
248            mSyncCallback = syncCallback;
249            mRouterObj = MediaRouterJellybean.getMediaRouter(context);
250            mCallbackObj = createCallbackObj();
251            mVolumeCallbackObj = createVolumeCallbackObj();
252
253            Resources r = context.getResources();
254            mUserRouteCategoryObj = MediaRouterJellybean.createRouteCategory(
255                    mRouterObj, r.getString(R.string.user_route_category_name), false);
256
257            updateSystemRoutes();
258        }
259
260        @Override
261        public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
262            int newRouteTypes = 0;
263            boolean newActiveScan = false;
264            if (request != null) {
265                final MediaRouteSelector selector = request.getSelector();
266                final List<String> categories = selector.getControlCategories();
267                final int count = categories.size();
268                for (int i = 0; i < count; i++) {
269                    String category = categories.get(i);
270                    if (category.equals(MediaControlIntent.CATEGORY_LIVE_AUDIO)) {
271                        newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO;
272                    } else if (category.equals(MediaControlIntent.CATEGORY_LIVE_VIDEO)) {
273                        newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO;
274                    } else {
275                        newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_USER;
276                    }
277                }
278                newActiveScan = request.isActiveScan();
279            }
280
281            if (mRouteTypes != newRouteTypes || mActiveScan != newActiveScan) {
282                mRouteTypes = newRouteTypes;
283                mActiveScan = newActiveScan;
284                updateCallback();
285                updateSystemRoutes();
286            }
287        }
288
289        @Override
290        public void onRouteAdded(Object routeObj) {
291            if (addSystemRouteNoPublish(routeObj)) {
292                publishRoutes();
293            }
294        }
295
296        private void updateSystemRoutes() {
297            boolean changed = false;
298            for (Object routeObj : MediaRouterJellybean.getRoutes(mRouterObj)) {
299                changed |= addSystemRouteNoPublish(routeObj);
300            }
301            if (changed) {
302                publishRoutes();
303            }
304        }
305
306        private boolean addSystemRouteNoPublish(Object routeObj) {
307            if (getUserRouteRecord(routeObj) == null
308                    && findSystemRouteRecord(routeObj) < 0) {
309                boolean isDefault = (getDefaultRoute() == routeObj);
310                SystemRouteRecord record = new SystemRouteRecord(routeObj, isDefault);
311                updateSystemRouteDescriptor(record);
312                mSystemRouteRecords.add(record);
313                return true;
314            }
315            return false;
316        }
317
318        @Override
319        public void onRouteRemoved(Object routeObj) {
320            if (getUserRouteRecord(routeObj) == null) {
321                int index = findSystemRouteRecord(routeObj);
322                if (index >= 0) {
323                    mSystemRouteRecords.remove(index);
324                    publishRoutes();
325                }
326            }
327        }
328
329        @Override
330        public void onRouteChanged(Object routeObj) {
331            if (getUserRouteRecord(routeObj) == null) {
332                int index = findSystemRouteRecord(routeObj);
333                if (index >= 0) {
334                    SystemRouteRecord record = mSystemRouteRecords.get(index);
335                    updateSystemRouteDescriptor(record);
336                    publishRoutes();
337                }
338            }
339        }
340
341        @Override
342        public void onRouteVolumeChanged(Object routeObj) {
343            if (getUserRouteRecord(routeObj) == null) {
344                int index = findSystemRouteRecord(routeObj);
345                if (index >= 0) {
346                    SystemRouteRecord record = mSystemRouteRecords.get(index);
347                    int newVolume = MediaRouterJellybean.RouteInfo.getVolume(routeObj);
348                    if (newVolume != record.mRouteDescriptor.getVolume()) {
349                        record.mRouteDescriptor =
350                                new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
351                                .setVolume(newVolume)
352                                .build();
353                        publishRoutes();
354                    }
355                }
356            }
357        }
358
359        @Override
360        public void onRouteSelected(int type, Object routeObj) {
361            if (routeObj != MediaRouterJellybean.getSelectedRoute(mRouterObj,
362                    MediaRouterJellybean.ALL_ROUTE_TYPES)) {
363                // The currently selected route has already changed so this callback
364                // is stale.  Drop it to prevent getting into sync loops.
365                return;
366            }
367
368            UserRouteRecord userRouteRecord = getUserRouteRecord(routeObj);
369            if (userRouteRecord != null) {
370                userRouteRecord.mRoute.select();
371            } else {
372                // Select the route if it already exists in the compat media router.
373                // If not, we will select it instead when the route is added.
374                int index = findSystemRouteRecord(routeObj);
375                if (index >= 0) {
376                    SystemRouteRecord record = mSystemRouteRecords.get(index);
377                    MediaRouter.RouteInfo route = mSyncCallback.getSystemRouteByDescriptorId(
378                            record.mRouteDescriptorId);
379                    if (route != null) {
380                        route.select();
381                    }
382                }
383            }
384        }
385
386        @Override
387        public void onRouteUnselected(int type, Object routeObj) {
388            // Nothing to do when a route is unselected.
389            // We only need to handle when a route is selected.
390        }
391
392        @Override
393        public void onRouteGrouped(Object routeObj, Object groupObj, int index) {
394            // Route grouping is deprecated and no longer supported.
395        }
396
397        @Override
398        public void onRouteUngrouped(Object routeObj, Object groupObj) {
399            // Route grouping is deprecated and no longer supported.
400        }
401
402        @Override
403        public void onVolumeSetRequest(Object routeObj, int volume) {
404            UserRouteRecord record = getUserRouteRecord(routeObj);
405            if (record != null) {
406                record.mRoute.requestSetVolume(volume);
407            }
408        }
409
410        @Override
411        public void onVolumeUpdateRequest(Object routeObj, int direction) {
412            UserRouteRecord record = getUserRouteRecord(routeObj);
413            if (record != null) {
414                record.mRoute.requestUpdateVolume(direction);
415            }
416        }
417
418        @Override
419        public void onSyncRouteAdded(MediaRouter.RouteInfo route) {
420            if (route.getProviderInstance() != this) {
421                Object routeObj = MediaRouterJellybean.createUserRoute(
422                        mRouterObj, mUserRouteCategoryObj);
423                UserRouteRecord record = new UserRouteRecord(route, routeObj);
424                MediaRouterJellybean.RouteInfo.setTag(routeObj, record);
425                MediaRouterJellybean.UserRouteInfo.setVolumeCallback(routeObj, mVolumeCallbackObj);
426                updateUserRouteProperties(record);
427                mUserRouteRecords.add(record);
428                MediaRouterJellybean.addUserRoute(mRouterObj, routeObj);
429            } else {
430                // If the newly added route is the counterpart of the currently selected
431                // route in the framework media router then ensure it is selected in
432                // the compat media router.
433                Object routeObj = MediaRouterJellybean.getSelectedRoute(
434                        mRouterObj, MediaRouterJellybean.ALL_ROUTE_TYPES);
435                int index = findSystemRouteRecord(routeObj);
436                if (index >= 0) {
437                    SystemRouteRecord record = mSystemRouteRecords.get(index);
438                    if (record.mRouteDescriptorId.equals(route.getDescriptorId())) {
439                        route.select();
440                    }
441                }
442            }
443        }
444
445        @Override
446        public void onSyncRouteRemoved(MediaRouter.RouteInfo route) {
447            if (route.getProviderInstance() != this) {
448                int index = findUserRouteRecord(route);
449                if (index >= 0) {
450                    UserRouteRecord record = mUserRouteRecords.remove(index);
451                    MediaRouterJellybean.RouteInfo.setTag(record.mRouteObj, null);
452                    MediaRouterJellybean.UserRouteInfo.setVolumeCallback(record.mRouteObj, null);
453                    MediaRouterJellybean.removeUserRoute(mRouterObj, record.mRouteObj);
454                }
455            }
456        }
457
458        @Override
459        public void onSyncRouteChanged(MediaRouter.RouteInfo route) {
460            if (route.getProviderInstance() != this) {
461                int index = findUserRouteRecord(route);
462                if (index >= 0) {
463                    UserRouteRecord record = mUserRouteRecords.get(index);
464                    updateUserRouteProperties(record);
465                }
466            }
467        }
468
469        @Override
470        public void onSyncRouteSelected(MediaRouter.RouteInfo route) {
471            if (!route.isSelected()) {
472                // The currently selected route has already changed so this callback
473                // is stale.  Drop it to prevent getting into sync loops.
474                return;
475            }
476
477            if (route.getProviderInstance() != this) {
478                int index = findUserRouteRecord(route);
479                if (index >= 0) {
480                    UserRouteRecord record = mUserRouteRecords.get(index);
481                    selectRoute(record.mRouteObj);
482                }
483            } else {
484                int index = findSystemRouteRecordByDescriptorId(route.getDescriptorId());
485                if (index >= 0) {
486                    SystemRouteRecord record = mSystemRouteRecords.get(index);
487                    selectRoute(record.mRouteObj);
488                }
489            }
490        }
491
492        protected void publishRoutes() {
493            MediaRouteProviderDescriptor.Builder builder =
494                    new MediaRouteProviderDescriptor.Builder()
495                    .addDiscoverableControlFilters(LIVE_AUDIO_CONTROL_FILTERS)
496                    .addDiscoverableControlFilters(LIVE_VIDEO_CONTROL_FILTERS);
497
498            int count = mSystemRouteRecords.size();
499            for (int i = 0; i < count; i++) {
500                builder.addRoute(mSystemRouteRecords.get(i).mRouteDescriptor);
501            }
502
503            setDescriptor(builder.build());
504        }
505
506        protected int findSystemRouteRecord(Object routeObj) {
507            final int count = mSystemRouteRecords.size();
508            for (int i = 0; i < count; i++) {
509                if (mSystemRouteRecords.get(i).mRouteObj == routeObj) {
510                    return i;
511                }
512            }
513            return -1;
514        }
515
516        protected int findSystemRouteRecordByDescriptorId(String id) {
517            final int count = mSystemRouteRecords.size();
518            for (int i = 0; i < count; i++) {
519                if (mSystemRouteRecords.get(i).mRouteDescriptorId.equals(id)) {
520                    return i;
521                }
522            }
523            return -1;
524        }
525
526        protected int findUserRouteRecord(MediaRouter.RouteInfo route) {
527            final int count = mUserRouteRecords.size();
528            for (int i = 0; i < count; i++) {
529                if (mUserRouteRecords.get(i).mRoute == route) {
530                    return i;
531                }
532            }
533            return -1;
534        }
535
536        protected UserRouteRecord getUserRouteRecord(Object routeObj) {
537            Object tag = MediaRouterJellybean.RouteInfo.getTag(routeObj);
538            return tag instanceof UserRouteRecord ? (UserRouteRecord)tag : null;
539        }
540
541        protected void updateSystemRouteDescriptor(SystemRouteRecord record) {
542            // We must always recreate the route descriptor when making any changes
543            // because they are intended to be immutable once published.
544
545            // Routes should not have null names but it may happen for badly configured
546            // user routes.  We tolerate this by using an empty name string here but
547            // such unnamed routes will be discarded by the media router upstream
548            // (with a log message so we can track down the problem).
549            CharSequence name = MediaRouterJellybean.RouteInfo.getName(
550                    record.mRouteObj, getContext());
551            MediaRouteDescriptor.Builder builder = new MediaRouteDescriptor.Builder(
552                    record.mRouteDescriptorId, name != null ? name.toString() : "");
553            onBuildSystemRouteDescriptor(record, builder);
554            record.mRouteDescriptor = builder.build();
555        }
556
557        protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
558                MediaRouteDescriptor.Builder builder) {
559            int supportedTypes = MediaRouterJellybean.RouteInfo.getSupportedTypes(
560                    record.mRouteObj);
561            if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO) != 0) {
562                builder.addControlFilters(LIVE_AUDIO_CONTROL_FILTERS);
563            }
564            if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
565                builder.addControlFilters(LIVE_VIDEO_CONTROL_FILTERS);
566            }
567
568            CharSequence status = MediaRouterJellybean.RouteInfo.getStatus(record.mRouteObj);
569            if (status != null) {
570                builder.setStatus(status.toString());
571            }
572            builder.setPlaybackType(
573                    MediaRouterJellybean.RouteInfo.getPlaybackType(record.mRouteObj));
574            builder.setPlaybackStream(
575                    MediaRouterJellybean.RouteInfo.getPlaybackStream(record.mRouteObj));
576            builder.setVolume(
577                    MediaRouterJellybean.RouteInfo.getVolume(record.mRouteObj));
578            builder.setVolumeMax(
579                    MediaRouterJellybean.RouteInfo.getVolumeMax(record.mRouteObj));
580            builder.setVolumeHandling(
581                    MediaRouterJellybean.RouteInfo.getVolumeHandling(record.mRouteObj));
582        }
583
584        protected void updateUserRouteProperties(UserRouteRecord record) {
585            MediaRouterJellybean.UserRouteInfo.setName(
586                    record.mRouteObj, record.mRoute.getName());
587            MediaRouterJellybean.UserRouteInfo.setStatus(
588                    record.mRouteObj, normalizeStatus(record.mRoute.getStatus()));
589            MediaRouterJellybean.UserRouteInfo.setPlaybackType(
590                    record.mRouteObj, record.mRoute.getPlaybackType());
591            MediaRouterJellybean.UserRouteInfo.setPlaybackStream(
592                    record.mRouteObj, record.mRoute.getPlaybackStream());
593            MediaRouterJellybean.UserRouteInfo.setVolume(
594                    record.mRouteObj, record.mRoute.getVolume());
595            MediaRouterJellybean.UserRouteInfo.setVolumeMax(
596                    record.mRouteObj, record.mRoute.getVolumeMax());
597            MediaRouterJellybean.UserRouteInfo.setVolumeHandling(
598                    record.mRouteObj, record.mRoute.getVolumeHandling());
599        }
600
601        protected void updateCallback() {
602            if (mCallbackRegistered) {
603                mCallbackRegistered = false;
604                MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj);
605            }
606
607            if (mRouteTypes != 0) {
608                mCallbackRegistered = true;
609                MediaRouterJellybean.addCallback(mRouterObj, mRouteTypes, mCallbackObj);
610            }
611        }
612
613        // The framework MediaRouter crashes if we set a null status even though
614        // RouteInfo.getStatus() may return null.  So we need to use a different
615        // value instead.
616        private static String normalizeStatus(String status) {
617            return status != null ? status : "";
618        }
619
620        protected Object createCallbackObj() {
621            return MediaRouterJellybean.createCallback(this);
622        }
623
624        protected Object createVolumeCallbackObj() {
625            return MediaRouterJellybean.createVolumeCallback(this);
626        }
627
628        protected void selectRoute(Object routeObj) {
629            if (mSelectRouteWorkaround == null) {
630                mSelectRouteWorkaround = new MediaRouterJellybean.SelectRouteWorkaround();
631            }
632            mSelectRouteWorkaround.selectRoute(mRouterObj,
633                    MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
634        }
635
636        protected Object getDefaultRoute() {
637            if (mGetDefaultRouteWorkaround == null) {
638                mGetDefaultRouteWorkaround = new MediaRouterJellybean.GetDefaultRouteWorkaround();
639            }
640            return mGetDefaultRouteWorkaround.getDefaultRoute(mRouterObj);
641        }
642
643        /**
644         * Represents a route that is provided by the framework media router
645         * and published by this route provider to the support library media router.
646         */
647        protected static final class SystemRouteRecord {
648            private static int sNextId;
649
650            public final Object mRouteObj;
651            public final String mRouteDescriptorId;
652            public MediaRouteDescriptor mRouteDescriptor; // assigned immediately after creation
653
654            public SystemRouteRecord(Object routeObj, boolean isDefault) {
655                mRouteObj = routeObj;
656                mRouteDescriptorId = isDefault ? DEFAULT_ROUTE_ID : "ROUTE_" + (sNextId++);
657            }
658        }
659
660        /**
661         * Represents a route that is provided by the support library media router
662         * and published by this route provider to the framework media router.
663         */
664        protected static final class UserRouteRecord {
665            public final MediaRouter.RouteInfo mRoute;
666            public final Object mRouteObj;
667
668            public UserRouteRecord(MediaRouter.RouteInfo route, Object routeObj) {
669                mRoute = route;
670                mRouteObj = routeObj;
671            }
672        }
673    }
674
675    /**
676     * Jellybean MR1 implementation.
677     */
678    private static class JellybeanMr1Impl extends JellybeanImpl
679            implements MediaRouterJellybeanMr1.Callback {
680        private MediaRouterJellybeanMr1.ActiveScanWorkaround mActiveScanWorkaround;
681        private MediaRouterJellybeanMr1.IsConnectingWorkaround mIsConnectingWorkaround;
682
683        public JellybeanMr1Impl(Context context, SyncCallback syncCallback) {
684            super(context, syncCallback);
685        }
686
687        @Override
688        public void onRoutePresentationDisplayChanged(Object routeObj) {
689            int index = findSystemRouteRecord(routeObj);
690            if (index >= 0) {
691                SystemRouteRecord record = mSystemRouteRecords.get(index);
692                Display newPresentationDisplay =
693                        MediaRouterJellybeanMr1.RouteInfo.getPresentationDisplay(routeObj);
694                int newPresentationDisplayId = (newPresentationDisplay != null
695                        ? newPresentationDisplay.getDisplayId() : -1);
696                if (newPresentationDisplayId
697                        != record.mRouteDescriptor.getPresentationDisplayId()) {
698                    record.mRouteDescriptor =
699                            new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
700                            .setPresentationDisplayId(newPresentationDisplayId)
701                            .build();
702                    publishRoutes();
703                }
704            }
705        }
706
707        @Override
708        protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
709                MediaRouteDescriptor.Builder builder) {
710            super.onBuildSystemRouteDescriptor(record, builder);
711
712            if (!MediaRouterJellybeanMr1.RouteInfo.isEnabled(record.mRouteObj)) {
713                builder.setEnabled(false);
714            }
715
716            if (isConnecting(record)) {
717                builder.setConnecting(true);
718            }
719
720            Display presentationDisplay =
721                    MediaRouterJellybeanMr1.RouteInfo.getPresentationDisplay(record.mRouteObj);
722            if (presentationDisplay != null) {
723                builder.setPresentationDisplayId(presentationDisplay.getDisplayId());
724            }
725        }
726
727        @Override
728        protected void updateCallback() {
729            super.updateCallback();
730
731            if (mActiveScanWorkaround == null) {
732                mActiveScanWorkaround = new MediaRouterJellybeanMr1.ActiveScanWorkaround(
733                        getContext(), getHandler());
734            }
735            mActiveScanWorkaround.setActiveScanRouteTypes(mActiveScan ? mRouteTypes : 0);
736        }
737
738        @Override
739        protected Object createCallbackObj() {
740            return MediaRouterJellybeanMr1.createCallback(this);
741        }
742
743        protected boolean isConnecting(SystemRouteRecord record) {
744            if (mIsConnectingWorkaround == null) {
745                mIsConnectingWorkaround = new MediaRouterJellybeanMr1.IsConnectingWorkaround();
746            }
747            return mIsConnectingWorkaround.isConnecting(record.mRouteObj);
748        }
749    }
750
751    /**
752     * Jellybean MR2 implementation.
753     */
754    private static class JellybeanMr2Impl extends JellybeanMr1Impl {
755        public JellybeanMr2Impl(Context context, SyncCallback syncCallback) {
756            super(context, syncCallback);
757        }
758
759        @Override
760        protected void selectRoute(Object routeObj) {
761            MediaRouterJellybean.selectRoute(mRouterObj,
762                    MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
763        }
764
765        @Override
766        protected Object getDefaultRoute() {
767            return MediaRouterJellybeanMr2.getDefaultRoute(mRouterObj);
768        }
769
770        @Override
771        protected void updateCallback() {
772            if (mCallbackRegistered) {
773                MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj);
774            }
775
776            mCallbackRegistered = true;
777            MediaRouterJellybeanMr2.addCallback(mRouterObj, mRouteTypes, mCallbackObj,
778                    MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS
779                    | (mActiveScan ? MediaRouter.CALLBACK_FLAG_ACTIVE_SCAN : 0));
780        }
781
782        @Override
783        protected boolean isConnecting(SystemRouteRecord record) {
784            return MediaRouterJellybeanMr2.RouteInfo.isConnecting(record.mRouteObj);
785        }
786    }
787}
788