| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.internal.app; |
| |
| import com.android.internal.R; |
| |
| import android.app.Activity; |
| import android.app.Dialog; |
| import android.app.DialogFragment; |
| import android.app.MediaRouteActionProvider; |
| import android.app.MediaRouteButton; |
| import android.content.Context; |
| import android.graphics.drawable.Drawable; |
| import android.hardware.display.DisplayManager; |
| import android.media.MediaRouter; |
| import android.media.MediaRouter.RouteCategory; |
| import android.media.MediaRouter.RouteGroup; |
| import android.media.MediaRouter.RouteInfo; |
| import android.os.Bundle; |
| import android.text.TextUtils; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.AdapterView; |
| import android.widget.BaseAdapter; |
| import android.widget.CheckBox; |
| import android.widget.Checkable; |
| import android.widget.ImageButton; |
| import android.widget.ImageView; |
| import android.widget.ListView; |
| import android.widget.SeekBar; |
| import android.widget.TextView; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| |
| /** |
| * This class implements the route chooser dialog for {@link MediaRouter}. |
| * |
| * @see MediaRouteButton |
| * @see MediaRouteActionProvider |
| */ |
| public class MediaRouteChooserDialogFragment extends DialogFragment { |
| private static final String TAG = "MediaRouteChooserDialogFragment"; |
| public static final String FRAGMENT_TAG = "android:MediaRouteChooserDialogFragment"; |
| |
| private static final int[] ITEM_LAYOUTS = new int[] { |
| R.layout.media_route_list_item_top_header, |
| R.layout.media_route_list_item_section_header, |
| R.layout.media_route_list_item, |
| R.layout.media_route_list_item_checkable, |
| R.layout.media_route_list_item_collapse_group |
| }; |
| |
| MediaRouter mRouter; |
| DisplayManager mDisplayService; |
| private int mRouteTypes; |
| |
| private LayoutInflater mInflater; |
| private LauncherListener mLauncherListener; |
| private View.OnClickListener mExtendedSettingsListener; |
| private RouteAdapter mAdapter; |
| private ListView mListView; |
| private SeekBar mVolumeSlider; |
| private ImageView mVolumeIcon; |
| |
| final RouteComparator mComparator = new RouteComparator(); |
| final MediaRouterCallback mCallback = new MediaRouterCallback(); |
| private boolean mIgnoreSliderVolumeChanges; |
| private boolean mIgnoreCallbackVolumeChanges; |
| |
| public MediaRouteChooserDialogFragment() { |
| setStyle(STYLE_NO_TITLE, R.style.Theme_DeviceDefault_Dialog); |
| } |
| |
| public void setLauncherListener(LauncherListener listener) { |
| mLauncherListener = listener; |
| } |
| |
| @Override |
| public void onAttach(Activity activity) { |
| super.onAttach(activity); |
| mRouter = (MediaRouter) activity.getSystemService(Context.MEDIA_ROUTER_SERVICE); |
| mDisplayService = (DisplayManager) activity.getSystemService(Context.DISPLAY_SERVICE); |
| } |
| |
| @Override |
| public void onDetach() { |
| super.onDetach(); |
| if (mLauncherListener != null) { |
| mLauncherListener.onDetached(this); |
| } |
| if (mAdapter != null) { |
| mAdapter = null; |
| } |
| mInflater = null; |
| mRouter.removeCallback(mCallback); |
| mRouter = null; |
| } |
| |
| public void setExtendedSettingsClickListener(View.OnClickListener listener) { |
| mExtendedSettingsListener = listener; |
| } |
| |
| public void setRouteTypes(int types) { |
| mRouteTypes = types; |
| if ((mRouteTypes & MediaRouter.ROUTE_TYPE_LIVE_VIDEO) != 0 && mDisplayService == null) { |
| final Context activity = getActivity(); |
| if (activity != null) { |
| mDisplayService = (DisplayManager) activity.getSystemService( |
| Context.DISPLAY_SERVICE); |
| } |
| } else { |
| mDisplayService = null; |
| } |
| } |
| |
| void updateVolume() { |
| if (mRouter == null) return; |
| |
| final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes); |
| mVolumeIcon.setImageResource( |
| selectedRoute.getPlaybackType() == RouteInfo.PLAYBACK_TYPE_LOCAL ? |
| R.drawable.ic_audio_vol : R.drawable.ic_media_route_on_holo_dark); |
| |
| mIgnoreSliderVolumeChanges = true; |
| |
| if (selectedRoute.getVolumeHandling() == RouteInfo.PLAYBACK_VOLUME_FIXED) { |
| // Disable the slider and show it at max volume. |
| mVolumeSlider.setMax(1); |
| mVolumeSlider.setProgress(1); |
| mVolumeSlider.setEnabled(false); |
| } else { |
| mVolumeSlider.setEnabled(true); |
| mVolumeSlider.setMax(selectedRoute.getVolumeMax()); |
| mVolumeSlider.setProgress(selectedRoute.getVolume()); |
| } |
| |
| mIgnoreSliderVolumeChanges = false; |
| } |
| |
| void changeVolume(int newValue) { |
| if (mIgnoreSliderVolumeChanges) return; |
| |
| final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes); |
| if (selectedRoute.getVolumeHandling() == RouteInfo.PLAYBACK_VOLUME_VARIABLE) { |
| final int maxVolume = selectedRoute.getVolumeMax(); |
| newValue = Math.max(0, Math.min(newValue, maxVolume)); |
| selectedRoute.requestSetVolume(newValue); |
| } |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| mInflater = inflater; |
| final View layout = inflater.inflate(R.layout.media_route_chooser_layout, container, false); |
| |
| mVolumeIcon = (ImageView) layout.findViewById(R.id.volume_icon); |
| mVolumeSlider = (SeekBar) layout.findViewById(R.id.volume_slider); |
| updateVolume(); |
| mVolumeSlider.setOnSeekBarChangeListener(new VolumeSliderChangeListener()); |
| |
| if (mExtendedSettingsListener != null) { |
| final View extendedSettingsButton = layout.findViewById(R.id.extended_settings); |
| extendedSettingsButton.setVisibility(View.VISIBLE); |
| extendedSettingsButton.setOnClickListener(mExtendedSettingsListener); |
| } |
| |
| final ListView list = (ListView) layout.findViewById(R.id.list); |
| list.setItemsCanFocus(true); |
| list.setAdapter(mAdapter = new RouteAdapter()); |
| list.setOnItemClickListener(mAdapter); |
| |
| mListView = list; |
| mRouter.addCallback(mRouteTypes, mCallback); |
| |
| mAdapter.scrollToSelectedItem(); |
| |
| return layout; |
| } |
| |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| return new RouteChooserDialog(getActivity(), getTheme()); |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| if (mDisplayService != null) { |
| mDisplayService.scanWifiDisplays(); |
| } |
| } |
| |
| private static class ViewHolder { |
| public TextView text1; |
| public TextView text2; |
| public ImageView icon; |
| public ImageButton expandGroupButton; |
| public RouteAdapter.ExpandGroupListener expandGroupListener; |
| public int position; |
| public CheckBox check; |
| } |
| |
| private class RouteAdapter extends BaseAdapter implements ListView.OnItemClickListener { |
| private static final int VIEW_TOP_HEADER = 0; |
| private static final int VIEW_SECTION_HEADER = 1; |
| private static final int VIEW_ROUTE = 2; |
| private static final int VIEW_GROUPING_ROUTE = 3; |
| private static final int VIEW_GROUPING_DONE = 4; |
| |
| private int mSelectedItemPosition = -1; |
| private final ArrayList<Object> mItems = new ArrayList<Object>(); |
| |
| private RouteCategory mCategoryEditingGroups; |
| private RouteGroup mEditingGroup; |
| |
| // Temporary lists for manipulation |
| private final ArrayList<RouteInfo> mCatRouteList = new ArrayList<RouteInfo>(); |
| private final ArrayList<RouteInfo> mSortRouteList = new ArrayList<RouteInfo>(); |
| |
| private boolean mIgnoreUpdates; |
| |
| RouteAdapter() { |
| update(); |
| } |
| |
| void update() { |
| /* |
| * This is kind of wacky, but our data sets are going to be |
| * fairly small on average. Ideally we should be able to do some of this stuff |
| * in-place instead. |
| * |
| * Basic idea: each entry in mItems represents an item in the list for quick access. |
| * Entries can be a RouteCategory (section header), a RouteInfo with a category of |
| * mCategoryEditingGroups (a flattened RouteInfo pulled out of its group, allowing |
| * the user to change the group), |
| */ |
| if (mIgnoreUpdates) return; |
| |
| mItems.clear(); |
| |
| final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes); |
| mSelectedItemPosition = -1; |
| |
| List<RouteInfo> routes; |
| final int catCount = mRouter.getCategoryCount(); |
| for (int i = 0; i < catCount; i++) { |
| final RouteCategory cat = mRouter.getCategoryAt(i); |
| routes = cat.getRoutes(mCatRouteList); |
| |
| if (!cat.isSystem()) { |
| mItems.add(cat); |
| } |
| |
| if (cat == mCategoryEditingGroups) { |
| addGroupEditingCategoryRoutes(routes); |
| } else { |
| addSelectableRoutes(selectedRoute, routes); |
| } |
| |
| routes.clear(); |
| } |
| |
| notifyDataSetChanged(); |
| if (mListView != null && mSelectedItemPosition >= 0) { |
| mListView.setItemChecked(mSelectedItemPosition, true); |
| } |
| } |
| |
| void scrollToEditingGroup() { |
| if (mCategoryEditingGroups == null || mListView == null) return; |
| |
| int pos = 0; |
| int bound = 0; |
| final int itemCount = mItems.size(); |
| for (int i = 0; i < itemCount; i++) { |
| final Object item = mItems.get(i); |
| if (item != null && item == mCategoryEditingGroups) { |
| bound = i; |
| } |
| if (item == null) { |
| pos = i; |
| break; // this is always below the category header; we can stop here. |
| } |
| } |
| |
| mListView.smoothScrollToPosition(pos, bound); |
| } |
| |
| void scrollToSelectedItem() { |
| if (mListView == null || mSelectedItemPosition < 0) return; |
| |
| mListView.smoothScrollToPosition(mSelectedItemPosition); |
| } |
| |
| void addSelectableRoutes(RouteInfo selectedRoute, List<RouteInfo> from) { |
| final int routeCount = from.size(); |
| for (int j = 0; j < routeCount; j++) { |
| final RouteInfo info = from.get(j); |
| if (info == selectedRoute) { |
| mSelectedItemPosition = mItems.size(); |
| } |
| mItems.add(info); |
| } |
| } |
| |
| void addGroupEditingCategoryRoutes(List<RouteInfo> from) { |
| // Unpack groups and flatten for presentation |
| // mSortRouteList will always be empty here. |
| final int topCount = from.size(); |
| for (int i = 0; i < topCount; i++) { |
| final RouteInfo route = from.get(i); |
| final RouteGroup group = route.getGroup(); |
| if (group == route) { |
| // This is a group, unpack it. |
| final int groupCount = group.getRouteCount(); |
| for (int j = 0; j < groupCount; j++) { |
| final RouteInfo innerRoute = group.getRouteAt(j); |
| mSortRouteList.add(innerRoute); |
| } |
| } else { |
| mSortRouteList.add(route); |
| } |
| } |
| // Sort by name. This will keep the route positions relatively stable even though they |
| // will be repeatedly added and removed. |
| Collections.sort(mSortRouteList, mComparator); |
| |
| mItems.addAll(mSortRouteList); |
| mSortRouteList.clear(); |
| |
| mItems.add(null); // Sentinel reserving space for the "done" button. |
| } |
| |
| @Override |
| public int getCount() { |
| return mItems.size(); |
| } |
| |
| @Override |
| public int getViewTypeCount() { |
| return 5; |
| } |
| |
| @Override |
| public int getItemViewType(int position) { |
| final Object item = getItem(position); |
| if (item instanceof RouteCategory) { |
| return position == 0 ? VIEW_TOP_HEADER : VIEW_SECTION_HEADER; |
| } else if (item == null) { |
| return VIEW_GROUPING_DONE; |
| } else { |
| final RouteInfo info = (RouteInfo) item; |
| if (info.getCategory() == mCategoryEditingGroups) { |
| return VIEW_GROUPING_ROUTE; |
| } |
| return VIEW_ROUTE; |
| } |
| } |
| |
| @Override |
| public boolean areAllItemsEnabled() { |
| return false; |
| } |
| |
| @Override |
| public boolean isEnabled(int position) { |
| switch (getItemViewType(position)) { |
| case VIEW_ROUTE: |
| return ((RouteInfo) mItems.get(position)).isEnabled(); |
| case VIEW_GROUPING_ROUTE: |
| case VIEW_GROUPING_DONE: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| @Override |
| public Object getItem(int position) { |
| return mItems.get(position); |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| return position; |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| final int viewType = getItemViewType(position); |
| |
| ViewHolder holder; |
| if (convertView == null) { |
| convertView = mInflater.inflate(ITEM_LAYOUTS[viewType], parent, false); |
| holder = new ViewHolder(); |
| holder.position = position; |
| holder.text1 = (TextView) convertView.findViewById(R.id.text1); |
| holder.text2 = (TextView) convertView.findViewById(R.id.text2); |
| holder.icon = (ImageView) convertView.findViewById(R.id.icon); |
| holder.check = (CheckBox) convertView.findViewById(R.id.check); |
| holder.expandGroupButton = (ImageButton) convertView.findViewById( |
| R.id.expand_button); |
| if (holder.expandGroupButton != null) { |
| holder.expandGroupListener = new ExpandGroupListener(); |
| holder.expandGroupButton.setOnClickListener(holder.expandGroupListener); |
| } |
| |
| final View fview = convertView; |
| final ListView list = (ListView) parent; |
| final ViewHolder fholder = holder; |
| convertView.setOnClickListener(new View.OnClickListener() { |
| @Override public void onClick(View v) { |
| list.performItemClick(fview, fholder.position, 0); |
| } |
| }); |
| convertView.setTag(holder); |
| } else { |
| holder = (ViewHolder) convertView.getTag(); |
| holder.position = position; |
| } |
| |
| switch (viewType) { |
| case VIEW_ROUTE: |
| case VIEW_GROUPING_ROUTE: |
| bindItemView(position, holder); |
| break; |
| case VIEW_SECTION_HEADER: |
| case VIEW_TOP_HEADER: |
| bindHeaderView(position, holder); |
| break; |
| } |
| |
| convertView.setActivated(position == mSelectedItemPosition); |
| convertView.setEnabled(isEnabled(position)); |
| |
| return convertView; |
| } |
| |
| void bindItemView(int position, ViewHolder holder) { |
| RouteInfo info = (RouteInfo) mItems.get(position); |
| holder.text1.setText(info.getName(getActivity())); |
| final CharSequence status = info.getStatus(); |
| if (TextUtils.isEmpty(status)) { |
| holder.text2.setVisibility(View.GONE); |
| } else { |
| holder.text2.setVisibility(View.VISIBLE); |
| holder.text2.setText(status); |
| } |
| Drawable icon = info.getIconDrawable(); |
| if (icon != null) { |
| // Make sure we have a fresh drawable where it doesn't matter if we mutate it |
| icon = icon.getConstantState().newDrawable(getResources()); |
| } |
| holder.icon.setImageDrawable(icon); |
| holder.icon.setVisibility(icon != null ? View.VISIBLE : View.GONE); |
| |
| RouteCategory cat = info.getCategory(); |
| boolean canGroup = false; |
| if (cat == mCategoryEditingGroups) { |
| RouteGroup group = info.getGroup(); |
| holder.check.setEnabled(group.getRouteCount() > 1); |
| holder.check.setChecked(group == mEditingGroup); |
| } else { |
| if (cat.isGroupable()) { |
| final RouteGroup group = (RouteGroup) info; |
| canGroup = group.getRouteCount() > 1 || |
| getItemViewType(position - 1) == VIEW_ROUTE || |
| (position < getCount() - 1 && |
| getItemViewType(position + 1) == VIEW_ROUTE); |
| } |
| } |
| |
| if (holder.expandGroupButton != null) { |
| holder.expandGroupButton.setVisibility(canGroup ? View.VISIBLE : View.GONE); |
| holder.expandGroupListener.position = position; |
| } |
| } |
| |
| void bindHeaderView(int position, ViewHolder holder) { |
| RouteCategory cat = (RouteCategory) mItems.get(position); |
| holder.text1.setText(cat.getName(getActivity())); |
| } |
| |
| @Override |
| public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |
| final int type = getItemViewType(position); |
| if (type == VIEW_SECTION_HEADER || type == VIEW_TOP_HEADER) { |
| return; |
| } else if (type == VIEW_GROUPING_DONE) { |
| finishGrouping(); |
| return; |
| } else { |
| final Object item = getItem(position); |
| if (!(item instanceof RouteInfo)) { |
| // Oops. Stale event running around? Skip it. |
| return; |
| } |
| |
| final RouteInfo route = (RouteInfo) item; |
| if (type == VIEW_ROUTE) { |
| mRouter.selectRouteInt(mRouteTypes, route); |
| dismiss(); |
| } else if (type == VIEW_GROUPING_ROUTE) { |
| final Checkable c = (Checkable) view; |
| final boolean wasChecked = c.isChecked(); |
| |
| mIgnoreUpdates = true; |
| RouteGroup oldGroup = route.getGroup(); |
| if (!wasChecked && oldGroup != mEditingGroup) { |
| // Assumption: in a groupable category oldGroup will never be null. |
| if (mRouter.getSelectedRoute(mRouteTypes) == oldGroup) { |
| // Old group was selected but is now empty. Select the group |
| // we're manipulating since that's where the last route went. |
| mRouter.selectRouteInt(mRouteTypes, mEditingGroup); |
| } |
| oldGroup.removeRoute(route); |
| mEditingGroup.addRoute(route); |
| c.setChecked(true); |
| } else if (wasChecked && mEditingGroup.getRouteCount() > 1) { |
| mEditingGroup.removeRoute(route); |
| |
| // In a groupable category this will add |
| // the route into its own new group. |
| mRouter.addRouteInt(route); |
| } |
| mIgnoreUpdates = false; |
| update(); |
| } |
| } |
| } |
| |
| boolean isGrouping() { |
| return mCategoryEditingGroups != null; |
| } |
| |
| void finishGrouping() { |
| mCategoryEditingGroups = null; |
| mEditingGroup = null; |
| getDialog().setCanceledOnTouchOutside(true); |
| update(); |
| scrollToSelectedItem(); |
| } |
| |
| class ExpandGroupListener implements View.OnClickListener { |
| int position; |
| |
| @Override |
| public void onClick(View v) { |
| // Assumption: this is only available for the user to click if we're presenting |
| // a groupable category, where every top-level route in the category is a group. |
| final RouteGroup group = (RouteGroup) getItem(position); |
| mEditingGroup = group; |
| mCategoryEditingGroups = group.getCategory(); |
| getDialog().setCanceledOnTouchOutside(false); |
| mRouter.selectRouteInt(mRouteTypes, mEditingGroup); |
| update(); |
| scrollToEditingGroup(); |
| } |
| } |
| } |
| |
| class MediaRouterCallback extends MediaRouter.Callback { |
| @Override |
| public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { |
| mAdapter.update(); |
| updateVolume(); |
| } |
| |
| @Override |
| public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { |
| mAdapter.update(); |
| } |
| |
| @Override |
| public void onRouteAdded(MediaRouter router, RouteInfo info) { |
| mAdapter.update(); |
| } |
| |
| @Override |
| public void onRouteRemoved(MediaRouter router, RouteInfo info) { |
| if (info == mAdapter.mEditingGroup) { |
| mAdapter.finishGrouping(); |
| } |
| mAdapter.update(); |
| } |
| |
| @Override |
| public void onRouteChanged(MediaRouter router, RouteInfo info) { |
| mAdapter.notifyDataSetChanged(); |
| } |
| |
| @Override |
| public void onRouteGrouped(MediaRouter router, RouteInfo info, |
| RouteGroup group, int index) { |
| mAdapter.update(); |
| } |
| |
| @Override |
| public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { |
| mAdapter.update(); |
| } |
| |
| @Override |
| public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) { |
| if (!mIgnoreCallbackVolumeChanges) { |
| updateVolume(); |
| } |
| } |
| } |
| |
| class RouteComparator implements Comparator<RouteInfo> { |
| @Override |
| public int compare(RouteInfo lhs, RouteInfo rhs) { |
| return lhs.getName(getActivity()).toString() |
| .compareTo(rhs.getName(getActivity()).toString()); |
| } |
| } |
| |
| class RouteChooserDialog extends Dialog { |
| public RouteChooserDialog(Context context, int theme) { |
| super(context, theme); |
| } |
| |
| @Override |
| public void onBackPressed() { |
| if (mAdapter != null && mAdapter.isGrouping()) { |
| mAdapter.finishGrouping(); |
| } else { |
| super.onBackPressed(); |
| } |
| } |
| |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && mVolumeSlider.isEnabled()) { |
| mRouter.getSelectedRoute(mRouteTypes).requestUpdateVolume(-1); |
| return true; |
| } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mVolumeSlider.isEnabled()) { |
| mRouter.getSelectedRoute(mRouteTypes).requestUpdateVolume(1); |
| return true; |
| } else { |
| return super.onKeyDown(keyCode, event); |
| } |
| } |
| |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && mVolumeSlider.isEnabled()) { |
| return true; |
| } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mVolumeSlider.isEnabled()) { |
| return true; |
| } else { |
| return super.onKeyUp(keyCode, event); |
| } |
| } |
| } |
| |
| /** |
| * Implemented by the MediaRouteButton that launched this dialog |
| */ |
| public interface LauncherListener { |
| public void onDetached(MediaRouteChooserDialogFragment detachedFragment); |
| } |
| |
| class VolumeSliderChangeListener implements SeekBar.OnSeekBarChangeListener { |
| |
| @Override |
| public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { |
| changeVolume(progress); |
| } |
| |
| @Override |
| public void onStartTrackingTouch(SeekBar seekBar) { |
| mIgnoreCallbackVolumeChanges = true; |
| } |
| |
| @Override |
| public void onStopTrackingTouch(SeekBar seekBar) { |
| mIgnoreCallbackVolumeChanges = false; |
| updateVolume(); |
| } |
| |
| } |
| } |