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