blob: 86129643686680b283a53193a6f744b6ac6fb7e1 [file] [log] [blame]
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001/*
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
Adam Powell823f0742011-09-21 17:17:01 -070019import com.android.internal.R;
20
Svetoslavbaeabb62013-10-28 15:22:14 -070021import android.content.ActivityNotFoundException;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070022import android.content.Context;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070023import android.content.Intent;
24import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
Svetoslav Ganov414051b2011-07-17 22:28:42 -070026import android.content.res.Resources;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070027import android.content.res.TypedArray;
28import android.database.DataSetObserver;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070029import android.graphics.drawable.Drawable;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070030import android.util.AttributeSet;
Svetoslavbaeabb62013-10-28 15:22:14 -070031import android.util.Log;
Adam Powell823f0742011-09-21 17:17:01 -070032import android.view.ActionProvider;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070033import android.view.LayoutInflater;
Alan Viverette5d46c892013-09-03 14:56:26 -070034import android.view.MotionEvent;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070035import android.view.View;
36import android.view.ViewGroup;
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -070037import android.view.ViewTreeObserver;
38import android.view.ViewTreeObserver.OnGlobalLayoutListener;
Alan Viverette058ac7c2013-08-19 16:44:30 -070039import android.view.accessibility.AccessibilityNodeInfo;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070040import android.widget.ActivityChooserModel.ActivityChooserModelClient;
Alan Viverette5d46c892013-09-03 14:56:26 -070041import android.widget.ListPopupWindow.ForwardingListener;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070042
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070043/**
44 * This class is a view for choosing an activity for handling a given {@link Intent}.
45 * <p>
46 * The view is composed of two adjacent buttons:
47 * <ul>
48 * <li>
49 * The left button is an immediate action and allows one click activity choosing.
50 * Tapping this button immediately executes the intent without requiring any further
51 * user input. Long press on this button shows a popup for changing the default
52 * activity.
53 * </li>
54 * <li>
55 * The right button is an overflow action and provides an optimized menu
56 * of additional activities. Tapping this button shows a popup anchored to this
57 * view, listing the most frequently used activities. This list is initially
58 * limited to a small number of items in frequency used order. The last item,
59 * "Show all..." serves as an affordance to display all available activities.
60 * </li>
61 * </ul>
62 * </p>
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070063 *
64 * @hide
65 */
66public class ActivityChooserView extends ViewGroup implements ActivityChooserModelClient {
67
Svetoslavbaeabb62013-10-28 15:22:14 -070068 private static final String LOG_TAG = "ActivityChooserView";
69
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070070 /**
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 /**
Svetoslav Ganovf2e75402011-09-07 16:38:40 -070086 * Stores the background drawable to allow hiding and latter showing.
87 */
88 private final Drawable mActivityChooserContentBackground;
89
90 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070091 * The expand activities action button;
92 */
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -070093 private final FrameLayout mExpandActivityOverflowButton;
94
95 /**
96 * The image for the expand activities action button;
97 */
98 private final ImageView mExpandActivityOverflowButtonImage;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070099
100 /**
101 * The default activities action button;
102 */
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700103 private final FrameLayout mDefaultActivityButton;
104
105 /**
106 * The image for the default activities action button;
107 */
108 private final ImageView mDefaultActivityButtonImage;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700109
110 /**
Svetoslav Ganov414051b2011-07-17 22:28:42 -0700111 * The maximal width of the list popup.
112 */
113 private final int mListPopupMaxWidth;
114
115 /**
Adam Powell823f0742011-09-21 17:17:01 -0700116 * The ActionProvider hosting this view, if applicable.
117 */
118 ActionProvider mProvider;
119
120 /**
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700121 * Observer for the model data.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700122 */
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700123 private final DataSetObserver mModelDataSetOberver = new DataSetObserver() {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700124
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700125 @Override
126 public void onChanged() {
127 super.onChanged();
128 mAdapter.notifyDataSetChanged();
129 }
130 @Override
131 public void onInvalidated() {
132 super.onInvalidated();
133 mAdapter.notifyDataSetInvalidated();
134 }
135 };
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700136
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700137 private final OnGlobalLayoutListener mOnGlobalLayoutListener = new OnGlobalLayoutListener() {
138 @Override
139 public void onGlobalLayout() {
140 if (isShowingPopup()) {
141 if (!isShown()) {
142 getListPopupWindow().dismiss();
143 } else {
144 getListPopupWindow().show();
Adam Powell823f0742011-09-21 17:17:01 -0700145 if (mProvider != null) {
146 mProvider.subUiVisibilityChanged(true);
147 }
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700148 }
149 }
150 }
151 };
152
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700153 /**
154 * Popup window for showing the activity overflow list.
155 */
156 private ListPopupWindow mListPopupWindow;
157
158 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700159 * Listener for the dismissal of the popup/alert.
160 */
161 private PopupWindow.OnDismissListener mOnDismissListener;
162
163 /**
164 * Flag whether a default activity currently being selected.
165 */
166 private boolean mIsSelectingDefaultActivity;
167
168 /**
169 * The count of activities in the popup.
170 */
171 private int mInitialActivityCount = ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT;
172
173 /**
174 * Flag whether this view is attached to a window.
175 */
176 private boolean mIsAttachedToWindow;
177
178 /**
Svetoslav Ganov70853772011-09-30 19:57:35 -0700179 * String resource for formatting content description of the default target.
180 */
181 private int mDefaultActionButtonContentDescription;
182
183 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700184 * Create a new instance.
185 *
186 * @param context The application environment.
187 */
188 public ActivityChooserView(Context context) {
189 this(context, null);
190 }
191
192 /**
193 * Create a new instance.
194 *
195 * @param context The application environment.
196 * @param attrs A collection of attributes.
197 */
198 public ActivityChooserView(Context context, AttributeSet attrs) {
Adam Powell23f4cc02011-08-18 10:30:46 -0700199 this(context, attrs, 0);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700200 }
201
202 /**
203 * Create a new instance.
204 *
205 * @param context The application environment.
206 * @param attrs A collection of attributes.
207 * @param defStyle The default style to apply to this view.
208 */
209 public ActivityChooserView(Context context, AttributeSet attrs, int defStyle) {
210 super(context, attrs, defStyle);
211
212 TypedArray attributesArray = context.obtainStyledAttributes(attrs,
213 R.styleable.ActivityChooserView, defStyle, 0);
214
215 mInitialActivityCount = attributesArray.getInt(
216 R.styleable.ActivityChooserView_initialActivityCount,
217 ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT);
218
219 Drawable expandActivityOverflowButtonDrawable = attributesArray.getDrawable(
220 R.styleable.ActivityChooserView_expandActivityOverflowButtonDrawable);
221
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700222 attributesArray.recycle();
223
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700224 LayoutInflater inflater = LayoutInflater.from(mContext);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700225 inflater.inflate(R.layout.activity_chooser_view, this, true);
226
227 mCallbacks = new Callbacks();
228
229 mActivityChooserContent = (LinearLayout) findViewById(R.id.activity_chooser_view_content);
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700230 mActivityChooserContentBackground = mActivityChooserContent.getBackground();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700231
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700232 mDefaultActivityButton = (FrameLayout) findViewById(R.id.default_activity_button);
233 mDefaultActivityButton.setOnClickListener(mCallbacks);
234 mDefaultActivityButton.setOnLongClickListener(mCallbacks);
235 mDefaultActivityButtonImage = (ImageView) mDefaultActivityButton.findViewById(R.id.image);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700236
Alan Viverette5d46c892013-09-03 14:56:26 -0700237 final FrameLayout expandButton = (FrameLayout) findViewById(R.id.expand_activities_button);
238 expandButton.setOnClickListener(mCallbacks);
239 expandButton.setAccessibilityDelegate(new AccessibilityDelegate() {
Alan Viverette058ac7c2013-08-19 16:44:30 -0700240 @Override
241 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
242 super.onInitializeAccessibilityNodeInfo(host, info);
Svetoslav Ganovcb8ed392013-08-23 20:37:28 -0700243 info.setCanOpenPopup(true);
Alan Viverette058ac7c2013-08-19 16:44:30 -0700244 }
245 });
Alan Viverette5d46c892013-09-03 14:56:26 -0700246 expandButton.setOnTouchListener(new ForwardingListener(expandButton) {
247 @Override
248 public ListPopupWindow getPopup() {
249 return getListPopupWindow();
250 }
251
252 @Override
253 protected boolean onForwardingStarted() {
254 showPopup();
255 return true;
256 }
257
258 @Override
259 protected boolean onForwardingStopped() {
260 dismissPopup();
261 return true;
262 }
263 });
264 mExpandActivityOverflowButton = expandButton;
265
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700266 mExpandActivityOverflowButtonImage =
Alan Viverette5d46c892013-09-03 14:56:26 -0700267 (ImageView) expandButton.findViewById(R.id.image);
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700268 mExpandActivityOverflowButtonImage.setImageDrawable(expandActivityOverflowButtonDrawable);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700269
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700270 mAdapter = new ActivityChooserViewAdapter();
271 mAdapter.registerDataSetObserver(new DataSetObserver() {
272 @Override
273 public void onChanged() {
274 super.onChanged();
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700275 updateAppearance();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700276 }
277 });
Svetoslav Ganov414051b2011-07-17 22:28:42 -0700278
279 Resources resources = context.getResources();
280 mListPopupMaxWidth = Math.max(resources.getDisplayMetrics().widthPixels / 2,
281 resources.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700282 }
283
284 /**
285 * {@inheritDoc}
286 */
287 public void setActivityChooserModel(ActivityChooserModel dataModel) {
288 mAdapter.setDataModel(dataModel);
289 if (isShowingPopup()) {
290 dismissPopup();
291 showPopup();
292 }
293 }
294
295 /**
296 * Sets the background for the button that expands the activity
297 * overflow list.
298 *
299 * <strong>Note:</strong> Clients would like to set this drawable
300 * as a clue about the action the chosen activity will perform. For
Svetoslav Ganov70853772011-09-30 19:57:35 -0700301 * example, if a share activity is to be chosen the drawable should
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700302 * give a clue that sharing is to be performed.
303 *
304 * @param drawable The drawable.
305 */
306 public void setExpandActivityOverflowButtonDrawable(Drawable drawable) {
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700307 mExpandActivityOverflowButtonImage.setImageDrawable(drawable);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700308 }
309
310 /**
Svetoslav Ganov70853772011-09-30 19:57:35 -0700311 * Sets the content description for the button that expands the activity
312 * overflow list.
313 *
314 * description as a clue about the action performed by the button.
315 * For example, if a share activity is to be chosen the content
316 * description should be something like "Share with".
317 *
318 * @param resourceId The content description resource id.
319 */
320 public void setExpandActivityOverflowButtonContentDescription(int resourceId) {
321 CharSequence contentDescription = mContext.getString(resourceId);
322 mExpandActivityOverflowButtonImage.setContentDescription(contentDescription);
323 }
324
325 /**
Adam Powell823f0742011-09-21 17:17:01 -0700326 * Set the provider hosting this view, if applicable.
327 * @hide Internal use only
328 */
329 public void setProvider(ActionProvider provider) {
330 mProvider = provider;
331 }
332
333 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700334 * Shows the popup window with activities.
335 *
336 * @return True if the popup was shown, false if already showing.
337 */
338 public boolean showPopup() {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700339 if (isShowingPopup() || !mIsAttachedToWindow) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700340 return false;
341 }
342 mIsSelectingDefaultActivity = false;
343 showPopupUnchecked(mInitialActivityCount);
344 return true;
345 }
346
347 /**
348 * Shows the popup no matter if it was already showing.
349 *
350 * @param maxActivityCount The max number of activities to display.
351 */
352 private void showPopupUnchecked(int maxActivityCount) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700353 if (mAdapter.getDataModel() == null) {
354 throw new IllegalStateException("No data model. Did you call #setDataModel?");
355 }
356
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700357 getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
358
Svetoslav Ganovf632eaa2011-09-22 16:05:12 -0700359 final boolean defaultActivityButtonShown =
360 mDefaultActivityButton.getVisibility() == VISIBLE;
361
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700362 final int activityCount = mAdapter.getActivityCount();
Svetoslav Ganovf632eaa2011-09-22 16:05:12 -0700363 final int maxActivityCountOffset = defaultActivityButtonShown ? 1 : 0;
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700364 if (maxActivityCount != ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED
Svetoslav Ganovf632eaa2011-09-22 16:05:12 -0700365 && activityCount > maxActivityCount + maxActivityCountOffset) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700366 mAdapter.setShowFooterView(true);
Svetoslav Ganov1c07e222011-09-29 16:50:59 -0700367 mAdapter.setMaxActivityCount(maxActivityCount - 1);
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700368 } else {
369 mAdapter.setShowFooterView(false);
Svetoslav Ganov1c07e222011-09-29 16:50:59 -0700370 mAdapter.setMaxActivityCount(maxActivityCount);
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700371 }
372
373 ListPopupWindow popupWindow = getListPopupWindow();
374 if (!popupWindow.isShowing()) {
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700375 if (mIsSelectingDefaultActivity || !defaultActivityButtonShown) {
376 mAdapter.setShowDefaultActivity(true, defaultActivityButtonShown);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700377 } else {
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700378 mAdapter.setShowDefaultActivity(false, false);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700379 }
Svetoslav Ganov414051b2011-07-17 22:28:42 -0700380 final int contentWidth = Math.min(mAdapter.measureContentWidth(), mListPopupMaxWidth);
381 popupWindow.setContentWidth(contentWidth);
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700382 popupWindow.show();
Adam Powell823f0742011-09-21 17:17:01 -0700383 if (mProvider != null) {
384 mProvider.subUiVisibilityChanged(true);
385 }
Svetoslav Ganov70853772011-09-30 19:57:35 -0700386 popupWindow.getListView().setContentDescription(mContext.getString(
387 R.string.activitychooserview_choose_application));
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700388 }
389 }
390
391 /**
392 * Dismisses the popup window with activities.
393 *
394 * @return True if dismissed, false if already dismissed.
395 */
396 public boolean dismissPopup() {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700397 if (isShowingPopup()) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700398 getListPopupWindow().dismiss();
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700399 ViewTreeObserver viewTreeObserver = getViewTreeObserver();
400 if (viewTreeObserver.isAlive()) {
Svetoslav Ganovaa47a6b2012-01-24 13:55:35 -0800401 viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700402 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700403 }
404 return true;
405 }
406
407 /**
408 * Gets whether the popup window with activities is shown.
409 *
410 * @return True if the popup is shown.
411 */
412 public boolean isShowingPopup() {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700413 return getListPopupWindow().isShowing();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700414 }
415
416 @Override
417 protected void onAttachedToWindow() {
418 super.onAttachedToWindow();
419 ActivityChooserModel dataModel = mAdapter.getDataModel();
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700420 if (dataModel != null) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700421 dataModel.registerObserver(mModelDataSetOberver);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700422 }
423 mIsAttachedToWindow = true;
424 }
425
426 @Override
427 protected void onDetachedFromWindow() {
428 super.onDetachedFromWindow();
429 ActivityChooserModel dataModel = mAdapter.getDataModel();
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700430 if (dataModel != null) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700431 dataModel.unregisterObserver(mModelDataSetOberver);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700432 }
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700433 ViewTreeObserver viewTreeObserver = getViewTreeObserver();
434 if (viewTreeObserver.isAlive()) {
Svetoslav Ganovaa47a6b2012-01-24 13:55:35 -0800435 viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700436 }
Svetoslav Ganov748ee2e2012-05-23 15:21:10 -0700437 if (isShowingPopup()) {
438 dismissPopup();
439 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700440 mIsAttachedToWindow = false;
441 }
442
443 @Override
444 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700445 View child = mActivityChooserContent;
446 // If the default action is not visible we want to be as tall as the
447 // ActionBar so if this widget is used in the latter it will look as
448 // a normal action button.
449 if (mDefaultActivityButton.getVisibility() != VISIBLE) {
450 heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
451 MeasureSpec.EXACTLY);
452 }
453 measureChild(child, widthMeasureSpec, heightMeasureSpec);
454 setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight());
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700455 }
456
457 @Override
458 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700459 mActivityChooserContent.layout(0, 0, right - left, bottom - top);
Svetoslav Ganov748ee2e2012-05-23 15:21:10 -0700460 if (!isShowingPopup()) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700461 dismissPopup();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700462 }
463 }
464
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700465 public ActivityChooserModel getDataModel() {
466 return mAdapter.getDataModel();
467 }
468
469 /**
470 * Sets a listener to receive a callback when the popup is dismissed.
471 *
472 * @param listener The listener to be notified.
473 */
474 public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
475 mOnDismissListener = listener;
476 }
477
478 /**
479 * Sets the initial count of items shown in the activities popup
480 * i.e. the items before the popup is expanded. This is an upper
481 * bound since it is not guaranteed that such number of intent
482 * handlers exist.
483 *
484 * @param itemCount The initial popup item count.
485 */
486 public void setInitialActivityCount(int itemCount) {
487 mInitialActivityCount = itemCount;
488 }
489
490 /**
Svetoslav Ganov70853772011-09-30 19:57:35 -0700491 * Sets a content description of the default action button. This
492 * resource should be a string taking one formatting argument and
493 * will be used for formatting the content description of the button
494 * dynamically as the default target changes. For example, a resource
495 * pointing to the string "share with %1$s" will result in a content
496 * description "share with Bluetooth" for the Bluetooth activity.
497 *
498 * @param resourceId The resource id.
499 */
500 public void setDefaultActionButtonContentDescription(int resourceId) {
501 mDefaultActionButtonContentDescription = resourceId;
502 }
503
504 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700505 * Gets the list popup window which is lazily initialized.
506 *
507 * @return The popup.
508 */
509 private ListPopupWindow getListPopupWindow() {
510 if (mListPopupWindow == null) {
511 mListPopupWindow = new ListPopupWindow(getContext());
512 mListPopupWindow.setAdapter(mAdapter);
513 mListPopupWindow.setAnchorView(ActivityChooserView.this);
514 mListPopupWindow.setModal(true);
515 mListPopupWindow.setOnItemClickListener(mCallbacks);
516 mListPopupWindow.setOnDismissListener(mCallbacks);
517 }
518 return mListPopupWindow;
519 }
520
521 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700522 * Updates the buttons state.
523 */
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700524 private void updateAppearance() {
525 // Expand overflow button.
526 if (mAdapter.getCount() > 0) {
527 mExpandActivityOverflowButton.setEnabled(true);
528 } else {
529 mExpandActivityOverflowButton.setEnabled(false);
530 }
531 // Default activity button.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700532 final int activityCount = mAdapter.getActivityCount();
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700533 final int historySize = mAdapter.getHistorySize();
fredc5a1195f2012-05-15 09:03:56 -0700534 if (activityCount==1 || activityCount > 1 && historySize > 0) {
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700535 mDefaultActivityButton.setVisibility(VISIBLE);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700536 ResolveInfo activity = mAdapter.getDefaultActivity();
537 PackageManager packageManager = mContext.getPackageManager();
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700538 mDefaultActivityButtonImage.setImageDrawable(activity.loadIcon(packageManager));
Svetoslav Ganov70853772011-09-30 19:57:35 -0700539 if (mDefaultActionButtonContentDescription != 0) {
540 CharSequence label = activity.loadLabel(packageManager);
541 String contentDescription = mContext.getString(
542 mDefaultActionButtonContentDescription, label);
543 mDefaultActivityButton.setContentDescription(contentDescription);
544 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700545 } else {
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700546 mDefaultActivityButton.setVisibility(View.GONE);
547 }
548 // Activity chooser content.
549 if (mDefaultActivityButton.getVisibility() == VISIBLE) {
Svetoslavbaeabb62013-10-28 15:22:14 -0700550 mActivityChooserContent.setBackground(mActivityChooserContentBackground);
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700551 } else {
Svetoslavbaeabb62013-10-28 15:22:14 -0700552 mActivityChooserContent.setBackground(null);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700553 }
554 }
555
556 /**
557 * Interface implementation to avoid publishing them in the APIs.
558 */
559 private class Callbacks implements AdapterView.OnItemClickListener,
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700560 View.OnClickListener, View.OnLongClickListener, PopupWindow.OnDismissListener {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700561
562 // AdapterView#OnItemClickListener
563 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
564 ActivityChooserViewAdapter adapter = (ActivityChooserViewAdapter) parent.getAdapter();
565 final int itemViewType = adapter.getItemViewType(position);
566 switch (itemViewType) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700567 case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_FOOTER: {
568 showPopupUnchecked(ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED);
569 } break;
570 case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_ACTIVITY: {
571 dismissPopup();
572 if (mIsSelectingDefaultActivity) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700573 // The item at position zero is the default already.
574 if (position > 0) {
575 mAdapter.getDataModel().setDefaultActivity(position);
576 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700577 } else {
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700578 // If the default target is not shown in the list, the first
579 // item in the model is default action => adjust index
580 position = mAdapter.getShowDefaultActivity() ? position : position + 1;
581 Intent launchIntent = mAdapter.getDataModel().chooseActivity(position);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700582 if (launchIntent != null) {
Adam Powell314419c2012-01-24 13:33:09 -0800583 launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Svetoslavbaeabb62013-10-28 15:22:14 -0700584 ResolveInfo resolveInfo = mAdapter.getDataModel().getActivity(position);
585 startActivity(launchIntent, resolveInfo);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700586 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700587 }
588 } break;
589 default:
590 throw new IllegalArgumentException();
591 }
592 }
593
594 // View.OnClickListener
595 public void onClick(View view) {
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700596 if (view == mDefaultActivityButton) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700597 dismissPopup();
598 ResolveInfo defaultActivity = mAdapter.getDefaultActivity();
599 final int index = mAdapter.getDataModel().getActivityIndex(defaultActivity);
600 Intent launchIntent = mAdapter.getDataModel().chooseActivity(index);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700601 if (launchIntent != null) {
Adam Powell314419c2012-01-24 13:33:09 -0800602 launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Svetoslavbaeabb62013-10-28 15:22:14 -0700603 startActivity(launchIntent, defaultActivity);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700604 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700605 } else if (view == mExpandActivityOverflowButton) {
606 mIsSelectingDefaultActivity = false;
607 showPopupUnchecked(mInitialActivityCount);
608 } else {
609 throw new IllegalArgumentException();
610 }
611 }
612
613 // OnLongClickListener#onLongClick
614 @Override
615 public boolean onLongClick(View view) {
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700616 if (view == mDefaultActivityButton) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700617 if (mAdapter.getCount() > 0) {
618 mIsSelectingDefaultActivity = true;
619 showPopupUnchecked(mInitialActivityCount);
620 }
621 } else {
622 throw new IllegalArgumentException();
623 }
624 return true;
625 }
626
627 // PopUpWindow.OnDismissListener#onDismiss
628 public void onDismiss() {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700629 notifyOnDismissListener();
Adam Powell823f0742011-09-21 17:17:01 -0700630 if (mProvider != null) {
631 mProvider.subUiVisibilityChanged(false);
632 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700633 }
634
635 private void notifyOnDismissListener() {
636 if (mOnDismissListener != null) {
637 mOnDismissListener.onDismiss();
638 }
639 }
Svetoslavbaeabb62013-10-28 15:22:14 -0700640
641 private void startActivity(Intent intent, ResolveInfo resolveInfo) {
642 try {
643 mContext.startActivity(intent);
644 } catch (RuntimeException re) {
645 CharSequence appLabel = resolveInfo.loadLabel(mContext.getPackageManager());
646 String message = mContext.getString(
647 R.string.activitychooserview_choose_application_error, appLabel);
648 Log.e(LOG_TAG, message);
649 Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
650 }
651 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700652 }
653
654 /**
655 * Adapter for backing the list of activities shown in the popup.
656 */
657 private class ActivityChooserViewAdapter extends BaseAdapter {
658
659 public static final int MAX_ACTIVITY_COUNT_UNLIMITED = Integer.MAX_VALUE;
660
661 public static final int MAX_ACTIVITY_COUNT_DEFAULT = 4;
662
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700663 private static final int ITEM_VIEW_TYPE_ACTIVITY = 0;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700664
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700665 private static final int ITEM_VIEW_TYPE_FOOTER = 1;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700666
667 private static final int ITEM_VIEW_TYPE_COUNT = 3;
668
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700669 private ActivityChooserModel mDataModel;
670
671 private int mMaxActivityCount = MAX_ACTIVITY_COUNT_DEFAULT;
672
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700673 private boolean mShowDefaultActivity;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700674
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700675 private boolean mHighlightDefaultActivity;
676
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700677 private boolean mShowFooterView;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700678
679 public void setDataModel(ActivityChooserModel dataModel) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700680 ActivityChooserModel oldDataModel = mAdapter.getDataModel();
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700681 if (oldDataModel != null && isShown()) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700682 oldDataModel.unregisterObserver(mModelDataSetOberver);
683 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700684 mDataModel = dataModel;
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700685 if (dataModel != null && isShown()) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700686 dataModel.registerObserver(mModelDataSetOberver);
687 }
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700688 notifyDataSetChanged();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700689 }
690
691 @Override
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700692 public int getItemViewType(int position) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700693 if (mShowFooterView && position == getCount() - 1) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700694 return ITEM_VIEW_TYPE_FOOTER;
695 } else {
696 return ITEM_VIEW_TYPE_ACTIVITY;
697 }
698 }
699
700 @Override
701 public int getViewTypeCount() {
702 return ITEM_VIEW_TYPE_COUNT;
703 }
704
705 public int getCount() {
706 int count = 0;
707 int activityCount = mDataModel.getActivityCount();
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700708 if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700709 activityCount--;
710 }
711 count = Math.min(activityCount, mMaxActivityCount);
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700712 if (mShowFooterView) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700713 count++;
714 }
715 return count;
716 }
717
718 public Object getItem(int position) {
719 final int itemViewType = getItemViewType(position);
720 switch (itemViewType) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700721 case ITEM_VIEW_TYPE_FOOTER:
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700722 return null;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700723 case ITEM_VIEW_TYPE_ACTIVITY:
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700724 if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) {
725 position++;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700726 }
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700727 return mDataModel.getActivity(position);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700728 default:
729 throw new IllegalArgumentException();
730 }
731 }
732
733 public long getItemId(int position) {
734 return position;
735 }
736
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700737 public View getView(int position, View convertView, ViewGroup parent) {
738 final int itemViewType = getItemViewType(position);
739 switch (itemViewType) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700740 case ITEM_VIEW_TYPE_FOOTER:
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700741 if (convertView == null || convertView.getId() != ITEM_VIEW_TYPE_FOOTER) {
742 convertView = LayoutInflater.from(getContext()).inflate(
743 R.layout.activity_chooser_view_list_item, parent, false);
744 convertView.setId(ITEM_VIEW_TYPE_FOOTER);
745 TextView titleView = (TextView) convertView.findViewById(R.id.title);
746 titleView.setText(mContext.getString(
747 R.string.activity_chooser_view_see_all));
748 }
749 return convertView;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700750 case ITEM_VIEW_TYPE_ACTIVITY:
751 if (convertView == null || convertView.getId() != R.id.list_item) {
752 convertView = LayoutInflater.from(getContext()).inflate(
753 R.layout.activity_chooser_view_list_item, parent, false);
754 }
755 PackageManager packageManager = mContext.getPackageManager();
756 // Set the icon
757 ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
758 ResolveInfo activity = (ResolveInfo) getItem(position);
Svetoslav Ganov414051b2011-07-17 22:28:42 -0700759 iconView.setImageDrawable(activity.loadIcon(packageManager));
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700760 // Set the title.
761 TextView titleView = (TextView) convertView.findViewById(R.id.title);
762 titleView.setText(activity.loadLabel(packageManager));
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700763 // Highlight the default.
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700764 if (mShowDefaultActivity && position == 0 && mHighlightDefaultActivity) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700765 convertView.setActivated(true);
766 } else {
767 convertView.setActivated(false);
768 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700769 return convertView;
770 default:
771 throw new IllegalArgumentException();
772 }
773 }
774
775 public int measureContentWidth() {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700776 // The user may have specified some of the target not to be shown but we
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700777 // want to measure all of them since after expansion they should fit.
778 final int oldMaxActivityCount = mMaxActivityCount;
779 mMaxActivityCount = MAX_ACTIVITY_COUNT_UNLIMITED;
780
781 int contentWidth = 0;
782 View itemView = null;
783
784 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
785 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
786 final int count = getCount();
787
788 for (int i = 0; i < count; i++) {
789 itemView = getView(i, itemView, null);
790 itemView.measure(widthMeasureSpec, heightMeasureSpec);
791 contentWidth = Math.max(contentWidth, itemView.getMeasuredWidth());
792 }
793
794 mMaxActivityCount = oldMaxActivityCount;
795
796 return contentWidth;
797 }
798
799 public void setMaxActivityCount(int maxActivityCount) {
800 if (mMaxActivityCount != maxActivityCount) {
801 mMaxActivityCount = maxActivityCount;
802 notifyDataSetChanged();
803 }
804 }
805
806 public ResolveInfo getDefaultActivity() {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700807 return mDataModel.getDefaultActivity();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700808 }
809
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700810 public void setShowFooterView(boolean showFooterView) {
811 if (mShowFooterView != showFooterView) {
812 mShowFooterView = showFooterView;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700813 notifyDataSetChanged();
814 }
815 }
816
817 public int getActivityCount() {
818 return mDataModel.getActivityCount();
819 }
820
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700821 public int getHistorySize() {
822 return mDataModel.getHistorySize();
823 }
824
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700825 public ActivityChooserModel getDataModel() {
826 return mDataModel;
827 }
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700828
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700829 public void setShowDefaultActivity(boolean showDefaultActivity,
830 boolean highlightDefaultActivity) {
831 if (mShowDefaultActivity != showDefaultActivity
832 || mHighlightDefaultActivity != highlightDefaultActivity) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700833 mShowDefaultActivity = showDefaultActivity;
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700834 mHighlightDefaultActivity = highlightDefaultActivity;
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700835 notifyDataSetChanged();
836 }
837 }
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700838
839 public boolean getShowDefaultActivity() {
840 return mShowDefaultActivity;
841 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700842 }
843}