blob: 2bc80ff717a3f1c1c4d1aab1155ce5b7eeebbe4a [file] [log] [blame]
/*
* 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 == null ||
selectedRoute.getPlaybackType() == RouteInfo.PLAYBACK_TYPE_LOCAL ?
R.drawable.ic_audio_vol : R.drawable.ic_media_route_on_holo_dark);
mIgnoreSliderVolumeChanges = true;
if (selectedRoute == null ||
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 != null &&
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()) {
final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes);
if (selectedRoute != null) {
selectedRoute.requestUpdateVolume(-1);
return true;
}
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mVolumeSlider.isEnabled()) {
final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes);
if (selectedRoute != null) {
mRouter.getSelectedRoute(mRouteTypes).requestUpdateVolume(1);
return true;
}
}
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();
}
}
}