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