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