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