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