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