ActivityChooserView.java revision 151763d3fc702ee2341aa6bebe821ce98d99e787
1/* 2 * Copyright (C) 2011 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 android.widget; 18 19import android.content.Context; 20import android.content.Intent; 21import android.content.pm.PackageManager; 22import android.content.pm.ResolveInfo; 23import android.content.res.Resources; 24import android.content.res.TypedArray; 25import android.database.DataSetObserver; 26import android.graphics.Canvas; 27import android.graphics.drawable.Drawable; 28import android.util.AttributeSet; 29import android.view.LayoutInflater; 30import android.view.View; 31import android.view.ViewGroup; 32import android.view.ViewTreeObserver; 33import android.view.ViewTreeObserver.OnGlobalLayoutListener; 34import android.widget.ActivityChooserModel.ActivityChooserModelClient; 35 36import com.android.internal.R; 37 38/** 39 * This class is a view for choosing an activity for handling a given {@link Intent}. 40 * <p> 41 * The view is composed of two adjacent buttons: 42 * <ul> 43 * <li> 44 * The left button is an immediate action and allows one click activity choosing. 45 * Tapping this button immediately executes the intent without requiring any further 46 * user input. Long press on this button shows a popup for changing the default 47 * activity. 48 * </li> 49 * <li> 50 * The right button is an overflow action and provides an optimized menu 51 * of additional activities. Tapping this button shows a popup anchored to this 52 * view, listing the most frequently used activities. This list is initially 53 * limited to a small number of items in frequency used order. The last item, 54 * "Show all..." serves as an affordance to display all available activities. 55 * </li> 56 * </ul> 57 * </p> 58 * 59 * @hide 60 */ 61public class ActivityChooserView extends ViewGroup implements ActivityChooserModelClient { 62 63 /** 64 * An adapter for displaying the activities in an {@link AdapterView}. 65 */ 66 private final ActivityChooserViewAdapter mAdapter; 67 68 /** 69 * Implementation of various interfaces to avoid publishing them in the APIs. 70 */ 71 private final Callbacks mCallbacks; 72 73 /** 74 * The content of this view. 75 */ 76 private final LinearLayout mActivityChooserContent; 77 78 /** 79 * The expand activities action button; 80 */ 81 private final FrameLayout mExpandActivityOverflowButton; 82 83 /** 84 * The image for the expand activities action button; 85 */ 86 private final ImageView mExpandActivityOverflowButtonImage; 87 88 /** 89 * The default activities action button; 90 */ 91 private final FrameLayout mDefaultActivityButton; 92 93 /** 94 * The image for the default activities action button; 95 */ 96 private final ImageView mDefaultActivityButtonImage; 97 98 /** 99 * The maximal width of the list popup. 100 */ 101 private final int mListPopupMaxWidth; 102 103 /** 104 * Observer for the model data. 105 */ 106 private final DataSetObserver mModelDataSetOberver = new DataSetObserver() { 107 108 @Override 109 public void onChanged() { 110 super.onChanged(); 111 mAdapter.notifyDataSetChanged(); 112 } 113 @Override 114 public void onInvalidated() { 115 super.onInvalidated(); 116 mAdapter.notifyDataSetInvalidated(); 117 } 118 }; 119 120 private final OnGlobalLayoutListener mOnGlobalLayoutListener = new OnGlobalLayoutListener() { 121 @Override 122 public void onGlobalLayout() { 123 if (isShowingPopup()) { 124 if (!isShown()) { 125 getListPopupWindow().dismiss(); 126 } else { 127 getListPopupWindow().show(); 128 } 129 } 130 } 131 }; 132 133 /** 134 * Popup window for showing the activity overflow list. 135 */ 136 private ListPopupWindow mListPopupWindow; 137 138 /** 139 * Listener for the dismissal of the popup/alert. 140 */ 141 private PopupWindow.OnDismissListener mOnDismissListener; 142 143 /** 144 * Flag whether a default activity currently being selected. 145 */ 146 private boolean mIsSelectingDefaultActivity; 147 148 /** 149 * The count of activities in the popup. 150 */ 151 private int mInitialActivityCount = ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT; 152 153 /** 154 * Flag whether this view is attached to a window. 155 */ 156 private boolean mIsAttachedToWindow; 157 158 /** 159 * Create a new instance. 160 * 161 * @param context The application environment. 162 */ 163 public ActivityChooserView(Context context) { 164 this(context, null); 165 } 166 167 /** 168 * Create a new instance. 169 * 170 * @param context The application environment. 171 * @param attrs A collection of attributes. 172 */ 173 public ActivityChooserView(Context context, AttributeSet attrs) { 174 this(context, attrs, 0); 175 } 176 177 /** 178 * Create a new instance. 179 * 180 * @param context The application environment. 181 * @param attrs A collection of attributes. 182 * @param defStyle The default style to apply to this view. 183 */ 184 public ActivityChooserView(Context context, AttributeSet attrs, int defStyle) { 185 super(context, attrs, defStyle); 186 187 TypedArray attributesArray = context.obtainStyledAttributes(attrs, 188 R.styleable.ActivityChooserView, defStyle, 0); 189 190 mInitialActivityCount = attributesArray.getInt( 191 R.styleable.ActivityChooserView_initialActivityCount, 192 ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT); 193 194 Drawable expandActivityOverflowButtonDrawable = attributesArray.getDrawable( 195 R.styleable.ActivityChooserView_expandActivityOverflowButtonDrawable); 196 197 LayoutInflater inflater = LayoutInflater.from(mContext); 198 inflater.inflate(R.layout.activity_chooser_view, this, true); 199 200 mCallbacks = new Callbacks(); 201 202 mActivityChooserContent = (LinearLayout) findViewById(R.id.activity_chooser_view_content); 203 204 mDefaultActivityButton = (FrameLayout) findViewById(R.id.default_activity_button); 205 mDefaultActivityButton.setOnClickListener(mCallbacks); 206 mDefaultActivityButton.setOnLongClickListener(mCallbacks); 207 mDefaultActivityButtonImage = (ImageView) mDefaultActivityButton.findViewById(R.id.image); 208 209 mExpandActivityOverflowButton = (FrameLayout) findViewById(R.id.expand_activities_button); 210 mExpandActivityOverflowButton.setOnClickListener(mCallbacks); 211 mExpandActivityOverflowButtonImage = 212 (ImageView) mExpandActivityOverflowButton.findViewById(R.id.image); 213 mExpandActivityOverflowButtonImage.setImageDrawable(expandActivityOverflowButtonDrawable); 214 215 mAdapter = new ActivityChooserViewAdapter(); 216 mAdapter.registerDataSetObserver(new DataSetObserver() { 217 @Override 218 public void onChanged() { 219 super.onChanged(); 220 updateButtons(); 221 } 222 }); 223 224 Resources resources = context.getResources(); 225 mListPopupMaxWidth = Math.max(resources.getDisplayMetrics().widthPixels / 2, 226 resources.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth)); 227 } 228 229 /** 230 * {@inheritDoc} 231 */ 232 public void setActivityChooserModel(ActivityChooserModel dataModel) { 233 mAdapter.setDataModel(dataModel); 234 if (isShowingPopup()) { 235 dismissPopup(); 236 showPopup(); 237 } 238 } 239 240 /** 241 * Sets the background for the button that expands the activity 242 * overflow list. 243 * 244 * <strong>Note:</strong> Clients would like to set this drawable 245 * as a clue about the action the chosen activity will perform. For 246 * example, if share activity is to be chosen the drawable should 247 * give a clue that sharing is to be performed. 248 * 249 * @param drawable The drawable. 250 */ 251 public void setExpandActivityOverflowButtonDrawable(Drawable drawable) { 252 mExpandActivityOverflowButtonImage.setImageDrawable(drawable); 253 } 254 255 /** 256 * Shows the popup window with activities. 257 * 258 * @return True if the popup was shown, false if already showing. 259 */ 260 public boolean showPopup() { 261 if (isShowingPopup() || !mIsAttachedToWindow) { 262 return false; 263 } 264 mIsSelectingDefaultActivity = false; 265 showPopupUnchecked(mInitialActivityCount); 266 return true; 267 } 268 269 /** 270 * Shows the popup no matter if it was already showing. 271 * 272 * @param maxActivityCount The max number of activities to display. 273 */ 274 private void showPopupUnchecked(int maxActivityCount) { 275 if (mAdapter.getDataModel() == null) { 276 throw new IllegalStateException("No data model. Did you call #setDataModel?"); 277 } 278 279 getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); 280 281 mAdapter.setMaxActivityCount(maxActivityCount); 282 283 final int activityCount = mAdapter.getActivityCount(); 284 if (maxActivityCount != ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED 285 && activityCount > maxActivityCount + 1) { 286 mAdapter.setShowFooterView(true); 287 } else { 288 mAdapter.setShowFooterView(false); 289 } 290 291 ListPopupWindow popupWindow = getListPopupWindow(); 292 if (!popupWindow.isShowing()) { 293 if (mIsSelectingDefaultActivity) { 294 mAdapter.setShowDefaultActivity(true); 295 } else { 296 mAdapter.setShowDefaultActivity(false); 297 } 298 final int contentWidth = Math.min(mAdapter.measureContentWidth(), mListPopupMaxWidth); 299 popupWindow.setContentWidth(contentWidth); 300 popupWindow.show(); 301 } 302 } 303 304 /** 305 * Dismisses the popup window with activities. 306 * 307 * @return True if dismissed, false if already dismissed. 308 */ 309 public boolean dismissPopup() { 310 if (isShowingPopup()) { 311 getListPopupWindow().dismiss(); 312 ViewTreeObserver viewTreeObserver = getViewTreeObserver(); 313 if (viewTreeObserver.isAlive()) { 314 viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener); 315 } 316 } 317 return true; 318 } 319 320 /** 321 * Gets whether the popup window with activities is shown. 322 * 323 * @return True if the popup is shown. 324 */ 325 public boolean isShowingPopup() { 326 return getListPopupWindow().isShowing(); 327 } 328 329 @Override 330 protected void onAttachedToWindow() { 331 super.onAttachedToWindow(); 332 ActivityChooserModel dataModel = mAdapter.getDataModel(); 333 if (dataModel != null) { 334 dataModel.registerObserver(mModelDataSetOberver); 335 } 336 mIsAttachedToWindow = true; 337 } 338 339 @Override 340 protected void onDetachedFromWindow() { 341 super.onDetachedFromWindow(); 342 ActivityChooserModel dataModel = mAdapter.getDataModel(); 343 if (dataModel != null) { 344 dataModel.unregisterObserver(mModelDataSetOberver); 345 } 346 ViewTreeObserver viewTreeObserver = getViewTreeObserver(); 347 if (viewTreeObserver.isAlive()) { 348 viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener); 349 } 350 mIsAttachedToWindow = false; 351 } 352 353 @Override 354 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 355 mActivityChooserContent.measure(widthMeasureSpec, heightMeasureSpec); 356 setMeasuredDimension(mActivityChooserContent.getMeasuredWidth(), 357 mActivityChooserContent.getMeasuredHeight()); 358 } 359 360 @Override 361 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 362 mActivityChooserContent.layout(0, 0, right - left, bottom - top); 363 if (getListPopupWindow().isShowing()) { 364 showPopupUnchecked(mAdapter.getMaxActivityCount()); 365 } else { 366 dismissPopup(); 367 } 368 } 369 370 @Override 371 protected void onDraw(Canvas canvas) { 372 mActivityChooserContent.onDraw(canvas); 373 } 374 375 public ActivityChooserModel getDataModel() { 376 return mAdapter.getDataModel(); 377 } 378 379 /** 380 * Sets a listener to receive a callback when the popup is dismissed. 381 * 382 * @param listener The listener to be notified. 383 */ 384 public void setOnDismissListener(PopupWindow.OnDismissListener listener) { 385 mOnDismissListener = listener; 386 } 387 388 /** 389 * Sets the initial count of items shown in the activities popup 390 * i.e. the items before the popup is expanded. This is an upper 391 * bound since it is not guaranteed that such number of intent 392 * handlers exist. 393 * 394 * @param itemCount The initial popup item count. 395 */ 396 public void setInitialActivityCount(int itemCount) { 397 mInitialActivityCount = itemCount; 398 } 399 400 /** 401 * Gets the list popup window which is lazily initialized. 402 * 403 * @return The popup. 404 */ 405 private ListPopupWindow getListPopupWindow() { 406 if (mListPopupWindow == null) { 407 mListPopupWindow = new ListPopupWindow(getContext()); 408 mListPopupWindow.setAdapter(mAdapter); 409 mListPopupWindow.setAnchorView(ActivityChooserView.this); 410 mListPopupWindow.setModal(true); 411 mListPopupWindow.setOnItemClickListener(mCallbacks); 412 mListPopupWindow.setOnDismissListener(mCallbacks); 413 } 414 return mListPopupWindow; 415 } 416 417 /** 418 * Updates the buttons state. 419 */ 420 private void updateButtons() { 421 final int activityCount = mAdapter.getActivityCount(); 422 if (activityCount > 0) { 423 mDefaultActivityButton.setVisibility(VISIBLE); 424 if (mAdapter.getCount() > 0) { 425 mExpandActivityOverflowButton.setEnabled(true); 426 } else { 427 mExpandActivityOverflowButton.setEnabled(false); 428 } 429 ResolveInfo activity = mAdapter.getDefaultActivity(); 430 PackageManager packageManager = mContext.getPackageManager(); 431 mDefaultActivityButtonImage.setImageDrawable(activity.loadIcon(packageManager)); 432 } else { 433 mDefaultActivityButton.setVisibility(View.INVISIBLE); 434 mExpandActivityOverflowButton.setEnabled(false); 435 } 436 } 437 438 /** 439 * Interface implementation to avoid publishing them in the APIs. 440 */ 441 private class Callbacks implements AdapterView.OnItemClickListener, 442 View.OnClickListener, View.OnLongClickListener, PopupWindow.OnDismissListener { 443 444 // AdapterView#OnItemClickListener 445 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 446 ActivityChooserViewAdapter adapter = (ActivityChooserViewAdapter) parent.getAdapter(); 447 final int itemViewType = adapter.getItemViewType(position); 448 switch (itemViewType) { 449 case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_FOOTER: { 450 showPopupUnchecked(ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED); 451 } break; 452 case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_ACTIVITY: { 453 dismissPopup(); 454 if (mIsSelectingDefaultActivity) { 455 // The item at position zero is the default already. 456 if (position > 0) { 457 mAdapter.getDataModel().setDefaultActivity(position); 458 } 459 } else { 460 // The first item in the model is default action => adjust index 461 Intent launchIntent = mAdapter.getDataModel().chooseActivity(position + 1); 462 if (launchIntent != null) { 463 mContext.startActivity(launchIntent); 464 } 465 } 466 } break; 467 default: 468 throw new IllegalArgumentException(); 469 } 470 } 471 472 // View.OnClickListener 473 public void onClick(View view) { 474 if (view == mDefaultActivityButton) { 475 dismissPopup(); 476 ResolveInfo defaultActivity = mAdapter.getDefaultActivity(); 477 final int index = mAdapter.getDataModel().getActivityIndex(defaultActivity); 478 Intent launchIntent = mAdapter.getDataModel().chooseActivity(index); 479 if (launchIntent != null) { 480 mContext.startActivity(launchIntent); 481 } 482 } else if (view == mExpandActivityOverflowButton) { 483 mIsSelectingDefaultActivity = false; 484 showPopupUnchecked(mInitialActivityCount); 485 } else { 486 throw new IllegalArgumentException(); 487 } 488 } 489 490 // OnLongClickListener#onLongClick 491 @Override 492 public boolean onLongClick(View view) { 493 if (view == mDefaultActivityButton) { 494 if (mAdapter.getCount() > 0) { 495 mIsSelectingDefaultActivity = true; 496 showPopupUnchecked(mInitialActivityCount); 497 } 498 } else { 499 throw new IllegalArgumentException(); 500 } 501 return true; 502 } 503 504 // PopUpWindow.OnDismissListener#onDismiss 505 public void onDismiss() { 506 notifyOnDismissListener(); 507 } 508 509 private void notifyOnDismissListener() { 510 if (mOnDismissListener != null) { 511 mOnDismissListener.onDismiss(); 512 } 513 } 514 } 515 516 /** 517 * Adapter for backing the list of activities shown in the popup. 518 */ 519 private class ActivityChooserViewAdapter extends BaseAdapter { 520 521 public static final int MAX_ACTIVITY_COUNT_UNLIMITED = Integer.MAX_VALUE; 522 523 public static final int MAX_ACTIVITY_COUNT_DEFAULT = 4; 524 525 private static final int ITEM_VIEW_TYPE_ACTIVITY = 0; 526 527 private static final int ITEM_VIEW_TYPE_FOOTER = 1; 528 529 private static final int ITEM_VIEW_TYPE_COUNT = 3; 530 531 private ActivityChooserModel mDataModel; 532 533 private int mMaxActivityCount = MAX_ACTIVITY_COUNT_DEFAULT; 534 535 private boolean mShowDefaultActivity; 536 537 private boolean mShowFooterView; 538 539 public void setDataModel(ActivityChooserModel dataModel) { 540 ActivityChooserModel oldDataModel = mAdapter.getDataModel(); 541 if (oldDataModel != null && isShown()) { 542 oldDataModel.unregisterObserver(mModelDataSetOberver); 543 } 544 mDataModel = dataModel; 545 if (dataModel != null && isShown()) { 546 dataModel.registerObserver(mModelDataSetOberver); 547 } 548 notifyDataSetChanged(); 549 } 550 551 @Override 552 public int getItemViewType(int position) { 553 if (mShowFooterView && position == getCount() - 1) { 554 return ITEM_VIEW_TYPE_FOOTER; 555 } else { 556 return ITEM_VIEW_TYPE_ACTIVITY; 557 } 558 } 559 560 @Override 561 public int getViewTypeCount() { 562 return ITEM_VIEW_TYPE_COUNT; 563 } 564 565 public int getCount() { 566 int count = 0; 567 int activityCount = mDataModel.getActivityCount(); 568 if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) { 569 activityCount--; 570 } 571 count = Math.min(activityCount, mMaxActivityCount); 572 if (mShowFooterView) { 573 count++; 574 } 575 return count; 576 } 577 578 public Object getItem(int position) { 579 final int itemViewType = getItemViewType(position); 580 switch (itemViewType) { 581 case ITEM_VIEW_TYPE_FOOTER: 582 return null; 583 case ITEM_VIEW_TYPE_ACTIVITY: 584 if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) { 585 position++; 586 } 587 return mDataModel.getActivity(position); 588 default: 589 throw new IllegalArgumentException(); 590 } 591 } 592 593 public long getItemId(int position) { 594 return position; 595 } 596 597 public View getView(int position, View convertView, ViewGroup parent) { 598 final int itemViewType = getItemViewType(position); 599 switch (itemViewType) { 600 case ITEM_VIEW_TYPE_FOOTER: 601 if (convertView == null || convertView.getId() != ITEM_VIEW_TYPE_FOOTER) { 602 convertView = LayoutInflater.from(getContext()).inflate( 603 R.layout.activity_chooser_view_list_item, parent, false); 604 convertView.setId(ITEM_VIEW_TYPE_FOOTER); 605 TextView titleView = (TextView) convertView.findViewById(R.id.title); 606 titleView.setText(mContext.getString( 607 R.string.activity_chooser_view_see_all)); 608 } 609 return convertView; 610 case ITEM_VIEW_TYPE_ACTIVITY: 611 if (convertView == null || convertView.getId() != R.id.list_item) { 612 convertView = LayoutInflater.from(getContext()).inflate( 613 R.layout.activity_chooser_view_list_item, parent, false); 614 } 615 PackageManager packageManager = mContext.getPackageManager(); 616 // Set the icon 617 ImageView iconView = (ImageView) convertView.findViewById(R.id.icon); 618 ResolveInfo activity = (ResolveInfo) getItem(position); 619 iconView.setImageDrawable(activity.loadIcon(packageManager)); 620 // Set the title. 621 TextView titleView = (TextView) convertView.findViewById(R.id.title); 622 titleView.setText(activity.loadLabel(packageManager)); 623 // Highlight the default. 624 if (mShowDefaultActivity && position == 0) { 625 convertView.setActivated(true); 626 } else { 627 convertView.setActivated(false); 628 } 629 return convertView; 630 default: 631 throw new IllegalArgumentException(); 632 } 633 } 634 635 public int measureContentWidth() { 636 // The user may have specified some of the target not to be shown but we 637 // want to measure all of them since after expansion they should fit. 638 final int oldMaxActivityCount = mMaxActivityCount; 639 mMaxActivityCount = MAX_ACTIVITY_COUNT_UNLIMITED; 640 641 int contentWidth = 0; 642 View itemView = null; 643 644 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 645 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 646 final int count = getCount(); 647 648 for (int i = 0; i < count; i++) { 649 itemView = getView(i, itemView, null); 650 itemView.measure(widthMeasureSpec, heightMeasureSpec); 651 contentWidth = Math.max(contentWidth, itemView.getMeasuredWidth()); 652 } 653 654 mMaxActivityCount = oldMaxActivityCount; 655 656 return contentWidth; 657 } 658 659 public void setMaxActivityCount(int maxActivityCount) { 660 if (mMaxActivityCount != maxActivityCount) { 661 mMaxActivityCount = maxActivityCount; 662 notifyDataSetChanged(); 663 } 664 } 665 666 public ResolveInfo getDefaultActivity() { 667 return mDataModel.getDefaultActivity(); 668 } 669 670 public void setShowFooterView(boolean showFooterView) { 671 if (mShowFooterView != showFooterView) { 672 mShowFooterView = showFooterView; 673 notifyDataSetChanged(); 674 } 675 } 676 677 public int getActivityCount() { 678 return mDataModel.getActivityCount(); 679 } 680 681 public int getMaxActivityCount() { 682 return mMaxActivityCount; 683 } 684 685 public ActivityChooserModel getDataModel() { 686 return mDataModel; 687 } 688 689 public void setShowDefaultActivity(boolean showDefaultActivity) { 690 if (mShowDefaultActivity != showDefaultActivity) { 691 mShowDefaultActivity = showDefaultActivity; 692 notifyDataSetChanged(); 693 } 694 } 695 } 696} 697