MediaRouteChooserDialogFragment.java revision 4599696591f745b3a546197d2ba7e5cfc5562484
1/*
2 * Copyright (C) 2012 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 com.android.internal.app;
18
19import com.android.internal.R;
20
21import android.app.Activity;
22import android.app.Dialog;
23import android.app.DialogFragment;
24import android.app.MediaRouteActionProvider;
25import android.app.MediaRouteButton;
26import android.content.Context;
27import android.graphics.drawable.Drawable;
28import android.media.AudioManager;
29import android.media.MediaRouter;
30import android.media.MediaRouter.RouteCategory;
31import android.media.MediaRouter.RouteGroup;
32import android.media.MediaRouter.RouteInfo;
33import android.media.MediaRouter.UserRouteInfo;
34import android.media.RemoteControlClient;
35import android.os.Bundle;
36import android.text.TextUtils;
37import android.view.KeyEvent;
38import android.view.LayoutInflater;
39import android.view.View;
40import android.view.ViewGroup;
41import android.widget.AdapterView;
42import android.widget.BaseAdapter;
43import android.widget.CheckBox;
44import android.widget.Checkable;
45import android.widget.ImageButton;
46import android.widget.ImageView;
47import android.widget.ListView;
48import android.widget.SeekBar;
49import android.widget.TextView;
50
51import java.util.ArrayList;
52import java.util.Collections;
53import java.util.Comparator;
54import java.util.List;
55
56/**
57 * This class implements the route chooser dialog for {@link MediaRouter}.
58 *
59 * @see MediaRouteButton
60 * @see MediaRouteActionProvider
61 */
62public class MediaRouteChooserDialogFragment extends DialogFragment {
63    private static final String TAG = "MediaRouteChooserDialogFragment";
64    public static final String FRAGMENT_TAG = "android:MediaRouteChooserDialogFragment";
65
66    private static final int[] ITEM_LAYOUTS = new int[] {
67        R.layout.media_route_list_item_top_header,
68        R.layout.media_route_list_item_section_header,
69        R.layout.media_route_list_item,
70        R.layout.media_route_list_item_checkable,
71        R.layout.media_route_list_item_collapse_group
72    };
73
74    MediaRouter mRouter;
75    AudioManager mAudio;
76    private int mRouteTypes;
77
78    private LayoutInflater mInflater;
79    private LauncherListener mLauncherListener;
80    private View.OnClickListener mExtendedSettingsListener;
81    private RouteAdapter mAdapter;
82    private ListView mListView;
83    private SeekBar mVolumeSlider;
84    private ImageView mVolumeIcon;
85
86    final RouteComparator mComparator = new RouteComparator();
87    final MediaRouterCallback mCallback = new MediaRouterCallback();
88    private boolean mIgnoreVolumeChanges;
89
90    public MediaRouteChooserDialogFragment() {
91        setStyle(STYLE_NO_TITLE, R.style.Theme_DeviceDefault_Dialog);
92    }
93
94    public void setLauncherListener(LauncherListener listener) {
95        mLauncherListener = listener;
96    }
97
98    @Override
99    public void onAttach(Activity activity) {
100        super.onAttach(activity);
101        mRouter = (MediaRouter) activity.getSystemService(Context.MEDIA_ROUTER_SERVICE);
102        mAudio = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE);
103    }
104
105    @Override
106    public void onDetach() {
107        super.onDetach();
108        if (mLauncherListener != null) {
109            mLauncherListener.onDetached(this);
110        }
111        if (mAdapter != null) {
112            mAdapter = null;
113        }
114        mInflater = null;
115        mRouter.removeCallback(mCallback);
116        mRouter = null;
117    }
118
119    public void setExtendedSettingsClickListener(View.OnClickListener listener) {
120        mExtendedSettingsListener = listener;
121    }
122
123    public void setRouteTypes(int types) {
124        mRouteTypes = types;
125    }
126
127    void updateVolume() {
128        final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes);
129        final boolean defaultAudioSelected = selectedRoute == mRouter.getSystemAudioRoute();
130        final boolean selectedSystemRoute =
131                selectedRoute.getCategory() == mRouter.getSystemAudioCategory();
132        mVolumeIcon.setImageResource(defaultAudioSelected ?
133                R.drawable.ic_audio_vol : R.drawable.ic_media_route_on_holo_dark);
134
135        mIgnoreVolumeChanges = true;
136        mVolumeSlider.setEnabled(true);
137        if (selectedSystemRoute) {
138            // Use the standard media audio stream
139            mVolumeSlider.setMax(mAudio.getStreamMaxVolume(AudioManager.STREAM_MUSIC));
140            mVolumeSlider.setProgress(mAudio.getStreamVolume(AudioManager.STREAM_MUSIC));
141        } else {
142            final RouteInfo firstSelected;
143            if (selectedRoute instanceof RouteGroup) {
144                firstSelected = ((RouteGroup) selectedRoute).getRouteAt(0);
145            } else {
146                firstSelected = selectedRoute;
147            }
148
149            RemoteControlClient rcc = null;
150            if (firstSelected instanceof UserRouteInfo) {
151                rcc = ((UserRouteInfo) firstSelected).getRemoteControlClient();
152            }
153
154            if (rcc == null) {
155                // No RemoteControlClient? Assume volume can't be controlled.
156                // Disable the slider and show it at max volume.
157                mVolumeSlider.setMax(1);
158                mVolumeSlider.setProgress(1);
159                mVolumeSlider.setEnabled(false);
160            } else {
161                // TODO: Connect this to the remote control volume
162            }
163        }
164        mIgnoreVolumeChanges = false;
165    }
166
167    void changeVolume(int newValue) {
168        if (mIgnoreVolumeChanges) return;
169
170        RouteCategory selectedCategory = mRouter.getSelectedRoute(mRouteTypes).getCategory();
171        if (selectedCategory == mRouter.getSystemAudioCategory()) {
172            final int maxVolume = mAudio.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
173            newValue = Math.max(0, Math.min(newValue, maxVolume));
174            mAudio.setStreamVolume(AudioManager.STREAM_MUSIC, newValue, 0);
175        }
176    }
177
178    @Override
179    public View onCreateView(LayoutInflater inflater, ViewGroup container,
180            Bundle savedInstanceState) {
181        mInflater = inflater;
182        final View layout = inflater.inflate(R.layout.media_route_chooser_layout, container, false);
183
184        mVolumeIcon = (ImageView) layout.findViewById(R.id.volume_icon);
185        mVolumeSlider = (SeekBar) layout.findViewById(R.id.volume_slider);
186        updateVolume();
187        mVolumeSlider.setOnSeekBarChangeListener(new VolumeSliderChangeListener());
188
189        if (mExtendedSettingsListener != null) {
190            final View extendedSettingsButton = layout.findViewById(R.id.extended_settings);
191            extendedSettingsButton.setVisibility(View.VISIBLE);
192            extendedSettingsButton.setOnClickListener(mExtendedSettingsListener);
193        }
194
195        final ListView list = (ListView) layout.findViewById(R.id.list);
196        list.setItemsCanFocus(true);
197        list.setAdapter(mAdapter = new RouteAdapter());
198        list.setOnItemClickListener(mAdapter);
199
200        mListView = list;
201        mRouter.addCallback(mRouteTypes, mCallback);
202
203        mAdapter.scrollToSelectedItem();
204
205        return layout;
206    }
207
208    @Override
209    public Dialog onCreateDialog(Bundle savedInstanceState) {
210        return new RouteChooserDialog(getActivity(), getTheme());
211    }
212
213    @Override
214    public void onResume() {
215        super.onResume();
216    }
217
218    private static class ViewHolder {
219        public TextView text1;
220        public TextView text2;
221        public ImageView icon;
222        public ImageButton expandGroupButton;
223        public RouteAdapter.ExpandGroupListener expandGroupListener;
224        public int position;
225        public CheckBox check;
226    }
227
228    private class RouteAdapter extends BaseAdapter implements ListView.OnItemClickListener {
229        private static final int VIEW_TOP_HEADER = 0;
230        private static final int VIEW_SECTION_HEADER = 1;
231        private static final int VIEW_ROUTE = 2;
232        private static final int VIEW_GROUPING_ROUTE = 3;
233        private static final int VIEW_GROUPING_DONE = 4;
234
235        private int mSelectedItemPosition = -1;
236        private final ArrayList<Object> mItems = new ArrayList<Object>();
237
238        private RouteCategory mCategoryEditingGroups;
239        private RouteGroup mEditingGroup;
240
241        // Temporary lists for manipulation
242        private final ArrayList<RouteInfo> mCatRouteList = new ArrayList<RouteInfo>();
243        private final ArrayList<RouteInfo> mSortRouteList = new ArrayList<RouteInfo>();
244
245        private boolean mIgnoreUpdates;
246
247        RouteAdapter() {
248            update();
249        }
250
251        void update() {
252            /*
253             * This is kind of wacky, but our data sets are going to be
254             * fairly small on average. Ideally we should be able to do some of this stuff
255             * in-place instead.
256             *
257             * Basic idea: each entry in mItems represents an item in the list for quick access.
258             * Entries can be a RouteCategory (section header), a RouteInfo with a category of
259             * mCategoryEditingGroups (a flattened RouteInfo pulled out of its group, allowing
260             * the user to change the group),
261             */
262            if (mIgnoreUpdates) return;
263
264            mItems.clear();
265
266            final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes);
267            mSelectedItemPosition = -1;
268
269            List<RouteInfo> routes;
270            final int catCount = mRouter.getCategoryCount();
271            for (int i = 0; i < catCount; i++) {
272                final RouteCategory cat = mRouter.getCategoryAt(i);
273                routes = cat.getRoutes(mCatRouteList);
274
275                mItems.add(cat);
276
277                if (cat == mCategoryEditingGroups) {
278                    addGroupEditingCategoryRoutes(routes);
279                } else {
280                    addSelectableRoutes(selectedRoute, routes);
281                }
282
283                routes.clear();
284            }
285
286            notifyDataSetChanged();
287            if (mListView != null && mSelectedItemPosition >= 0) {
288                mListView.setItemChecked(mSelectedItemPosition, true);
289            }
290        }
291
292        void scrollToEditingGroup() {
293            if (mCategoryEditingGroups == null || mListView == null) return;
294
295            int pos = 0;
296            int bound = 0;
297            final int itemCount = mItems.size();
298            for (int i = 0; i < itemCount; i++) {
299                final Object item = mItems.get(i);
300                if (item != null && item == mCategoryEditingGroups) {
301                    bound = i;
302                }
303                if (item == null) {
304                    pos = i;
305                    break; // this is always below the category header; we can stop here.
306                }
307            }
308
309            mListView.smoothScrollToPosition(pos, bound);
310        }
311
312        void scrollToSelectedItem() {
313            if (mListView == null || mSelectedItemPosition < 0) return;
314
315            mListView.smoothScrollToPosition(mSelectedItemPosition);
316        }
317
318        void addSelectableRoutes(RouteInfo selectedRoute, List<RouteInfo> from) {
319            final int routeCount = from.size();
320            for (int j = 0; j < routeCount; j++) {
321                final RouteInfo info = from.get(j);
322                if (info == selectedRoute) {
323                    mSelectedItemPosition = mItems.size();
324                }
325                mItems.add(info);
326            }
327        }
328
329        void addGroupEditingCategoryRoutes(List<RouteInfo> from) {
330            // Unpack groups and flatten for presentation
331            // mSortRouteList will always be empty here.
332            final int topCount = from.size();
333            for (int i = 0; i < topCount; i++) {
334                final RouteInfo route = from.get(i);
335                final RouteGroup group = route.getGroup();
336                if (group == route) {
337                    // This is a group, unpack it.
338                    final int groupCount = group.getRouteCount();
339                    for (int j = 0; j < groupCount; j++) {
340                        final RouteInfo innerRoute = group.getRouteAt(j);
341                        mSortRouteList.add(innerRoute);
342                    }
343                } else {
344                    mSortRouteList.add(route);
345                }
346            }
347            // Sort by name. This will keep the route positions relatively stable even though they
348            // will be repeatedly added and removed.
349            Collections.sort(mSortRouteList, mComparator);
350
351            mItems.addAll(mSortRouteList);
352            mSortRouteList.clear();
353
354            mItems.add(null); // Sentinel reserving space for the "done" button.
355        }
356
357        @Override
358        public int getCount() {
359            return mItems.size();
360        }
361
362        @Override
363        public int getViewTypeCount() {
364            return 5;
365        }
366
367        @Override
368        public int getItemViewType(int position) {
369            final Object item = getItem(position);
370            if (item instanceof RouteCategory) {
371                return position == 0 ? VIEW_TOP_HEADER : VIEW_SECTION_HEADER;
372            } else if (item == null) {
373                return VIEW_GROUPING_DONE;
374            } else {
375                final RouteInfo info = (RouteInfo) item;
376                if (info.getCategory() == mCategoryEditingGroups) {
377                    return VIEW_GROUPING_ROUTE;
378                }
379                return VIEW_ROUTE;
380            }
381        }
382
383        @Override
384        public boolean areAllItemsEnabled() {
385            return false;
386        }
387
388        @Override
389        public boolean isEnabled(int position) {
390            switch (getItemViewType(position)) {
391                case VIEW_ROUTE:
392                case VIEW_GROUPING_ROUTE:
393                case VIEW_GROUPING_DONE:
394                    return true;
395                default:
396                    return false;
397            }
398        }
399
400        @Override
401        public Object getItem(int position) {
402            return mItems.get(position);
403        }
404
405        @Override
406        public long getItemId(int position) {
407            return position;
408        }
409
410        @Override
411        public View getView(int position, View convertView, ViewGroup parent) {
412            final int viewType = getItemViewType(position);
413
414            ViewHolder holder;
415            if (convertView == null) {
416                convertView = mInflater.inflate(ITEM_LAYOUTS[viewType], parent, false);
417                holder = new ViewHolder();
418                holder.position = position;
419                holder.text1 = (TextView) convertView.findViewById(R.id.text1);
420                holder.text2 = (TextView) convertView.findViewById(R.id.text2);
421                holder.icon = (ImageView) convertView.findViewById(R.id.icon);
422                holder.check = (CheckBox) convertView.findViewById(R.id.check);
423                holder.expandGroupButton = (ImageButton) convertView.findViewById(
424                        R.id.expand_button);
425                if (holder.expandGroupButton != null) {
426                    holder.expandGroupListener = new ExpandGroupListener();
427                    holder.expandGroupButton.setOnClickListener(holder.expandGroupListener);
428                }
429
430                final View fview = convertView;
431                final ListView list = (ListView) parent;
432                final ViewHolder fholder = holder;
433                convertView.setOnClickListener(new View.OnClickListener() {
434                    @Override public void onClick(View v) {
435                        list.performItemClick(fview, fholder.position, 0);
436                    }
437                });
438                convertView.setTag(holder);
439            } else {
440                holder = (ViewHolder) convertView.getTag();
441                holder.position = position;
442            }
443
444            switch (viewType) {
445                case VIEW_ROUTE:
446                case VIEW_GROUPING_ROUTE:
447                    bindItemView(position, holder);
448                    break;
449                case VIEW_SECTION_HEADER:
450                case VIEW_TOP_HEADER:
451                    bindHeaderView(position, holder);
452                    break;
453            }
454
455            convertView.setActivated(position == mSelectedItemPosition);
456
457            return convertView;
458        }
459
460        void bindItemView(int position, ViewHolder holder) {
461            RouteInfo info = (RouteInfo) mItems.get(position);
462            holder.text1.setText(info.getName(getActivity()));
463            final CharSequence status = info.getStatus();
464            if (TextUtils.isEmpty(status)) {
465                holder.text2.setVisibility(View.GONE);
466            } else {
467                holder.text2.setVisibility(View.VISIBLE);
468                holder.text2.setText(status);
469            }
470            Drawable icon = info.getIconDrawable();
471            if (icon != null) {
472                // Make sure we have a fresh drawable where it doesn't matter if we mutate it
473                icon = icon.getConstantState().newDrawable(getResources());
474            }
475            holder.icon.setImageDrawable(icon);
476            holder.icon.setVisibility(icon != null ? View.VISIBLE : View.GONE);
477
478            RouteCategory cat = info.getCategory();
479            boolean canGroup = false;
480            if (cat == mCategoryEditingGroups) {
481                RouteGroup group = info.getGroup();
482                holder.check.setEnabled(group.getRouteCount() > 1);
483                holder.check.setChecked(group == mEditingGroup);
484            } else {
485                if (cat.isGroupable()) {
486                    final RouteGroup group = (RouteGroup) info;
487                    canGroup = group.getRouteCount() > 1 ||
488                            getItemViewType(position - 1) == VIEW_ROUTE ||
489                            (position < getCount() - 1 &&
490                                    getItemViewType(position + 1) == VIEW_ROUTE);
491                }
492            }
493
494            if (holder.expandGroupButton != null) {
495                holder.expandGroupButton.setVisibility(canGroup ? View.VISIBLE : View.GONE);
496                holder.expandGroupListener.position = position;
497            }
498        }
499
500        void bindHeaderView(int position, ViewHolder holder) {
501            RouteCategory cat = (RouteCategory) mItems.get(position);
502            holder.text1.setText(cat.getName(getActivity()));
503        }
504
505        @Override
506        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
507            final int type = getItemViewType(position);
508            if (type == VIEW_SECTION_HEADER || type == VIEW_TOP_HEADER) {
509                return;
510            } else if (type == VIEW_GROUPING_DONE) {
511                finishGrouping();
512                return;
513            } else {
514                final Object item = getItem(position);
515                if (!(item instanceof RouteInfo)) {
516                    // Oops. Stale event running around? Skip it.
517                    return;
518                }
519
520                final RouteInfo route = (RouteInfo) item;
521                if (type == VIEW_ROUTE) {
522                    mRouter.selectRouteInt(mRouteTypes, route);
523                    dismiss();
524                } else if (type == VIEW_GROUPING_ROUTE) {
525                    final Checkable c = (Checkable) view;
526                    final boolean wasChecked = c.isChecked();
527
528                    mIgnoreUpdates = true;
529                    RouteGroup oldGroup = route.getGroup();
530                    if (!wasChecked && oldGroup != mEditingGroup) {
531                        // Assumption: in a groupable category oldGroup will never be null.
532                        if (mRouter.getSelectedRoute(mRouteTypes) == oldGroup) {
533                            // Old group was selected but is now empty. Select the group
534                            // we're manipulating since that's where the last route went.
535                            mRouter.selectRouteInt(mRouteTypes, mEditingGroup);
536                        }
537                        oldGroup.removeRoute(route);
538                        mEditingGroup.addRoute(route);
539                        c.setChecked(true);
540                    } else if (wasChecked && mEditingGroup.getRouteCount() > 1) {
541                        mEditingGroup.removeRoute(route);
542
543                        // In a groupable category this will add
544                        // the route into its own new group.
545                        mRouter.addRouteInt(route);
546                    }
547                    mIgnoreUpdates = false;
548                    update();
549                }
550            }
551        }
552
553        boolean isGrouping() {
554            return mCategoryEditingGroups != null;
555        }
556
557        void finishGrouping() {
558            mCategoryEditingGroups = null;
559            mEditingGroup = null;
560            getDialog().setCanceledOnTouchOutside(true);
561            update();
562            scrollToSelectedItem();
563        }
564
565        class ExpandGroupListener implements View.OnClickListener {
566            int position;
567
568            @Override
569            public void onClick(View v) {
570                // Assumption: this is only available for the user to click if we're presenting
571                // a groupable category, where every top-level route in the category is a group.
572                final RouteGroup group = (RouteGroup) getItem(position);
573                mEditingGroup = group;
574                mCategoryEditingGroups = group.getCategory();
575                getDialog().setCanceledOnTouchOutside(false);
576                mRouter.selectRouteInt(mRouteTypes, mEditingGroup);
577                update();
578                scrollToEditingGroup();
579            }
580        }
581    }
582
583    class MediaRouterCallback extends MediaRouter.Callback {
584        @Override
585        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
586            mAdapter.update();
587            updateVolume();
588        }
589
590        @Override
591        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
592            mAdapter.update();
593        }
594
595        @Override
596        public void onRouteAdded(MediaRouter router, RouteInfo info) {
597            mAdapter.update();
598            updateVolume();
599        }
600
601        @Override
602        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
603            if (info == mAdapter.mEditingGroup) {
604                mAdapter.finishGrouping();
605            }
606            mAdapter.update();
607            updateVolume();
608        }
609
610        @Override
611        public void onRouteChanged(MediaRouter router, RouteInfo info) {
612            mAdapter.notifyDataSetChanged();
613        }
614
615        @Override
616        public void onRouteGrouped(MediaRouter router, RouteInfo info,
617                RouteGroup group, int index) {
618            mAdapter.update();
619        }
620
621        @Override
622        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
623            mAdapter.update();
624        }
625    }
626
627    class RouteComparator implements Comparator<RouteInfo> {
628        @Override
629        public int compare(RouteInfo lhs, RouteInfo rhs) {
630            return lhs.getName(getActivity()).toString()
631                    .compareTo(rhs.getName(getActivity()).toString());
632        }
633    }
634
635    class RouteChooserDialog extends Dialog {
636        public RouteChooserDialog(Context context, int theme) {
637            super(context, theme);
638        }
639
640        @Override
641        public void onBackPressed() {
642            if (mAdapter != null && mAdapter.isGrouping()) {
643                mAdapter.finishGrouping();
644            } else {
645                super.onBackPressed();
646            }
647        }
648
649        public boolean onKeyDown(int keyCode, KeyEvent event) {
650            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && mVolumeSlider.isEnabled()) {
651                mVolumeSlider.incrementProgressBy(-1);
652                return true;
653            } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mVolumeSlider.isEnabled()) {
654                mVolumeSlider.incrementProgressBy(1);
655                return true;
656            } else {
657                return super.onKeyDown(keyCode, event);
658            }
659        }
660    }
661
662    /**
663     * Implemented by the MediaRouteButton that launched this dialog
664     */
665    public interface LauncherListener {
666        public void onDetached(MediaRouteChooserDialogFragment detachedFragment);
667    }
668
669    class VolumeSliderChangeListener implements SeekBar.OnSeekBarChangeListener {
670
671        @Override
672        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
673            changeVolume(progress);
674        }
675
676        @Override
677        public void onStartTrackingTouch(SeekBar seekBar) {
678        }
679
680        @Override
681        public void onStopTrackingTouch(SeekBar seekBar) {
682        }
683
684    }
685}
686