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