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