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