blob: f9af2f9ed38d73ae7361329b80b98a473734766d [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
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070021import android.content.Context;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070022import android.content.Intent;
23import android.content.pm.PackageManager;
24import android.content.pm.ResolveInfo;
Svetoslav Ganov414051b2011-07-17 22:28:42 -070025import android.content.res.Resources;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070026import android.content.res.TypedArray;
27import android.database.DataSetObserver;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070028import android.graphics.drawable.Drawable;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070029import android.util.AttributeSet;
Svetoslavbaeabb62013-10-28 15:22:14 -070030import android.util.Log;
Adam Powell823f0742011-09-21 17:17:01 -070031import android.view.ActionProvider;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070032import android.view.LayoutInflater;
33import android.view.View;
34import android.view.ViewGroup;
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -070035import android.view.ViewTreeObserver;
36import android.view.ViewTreeObserver.OnGlobalLayoutListener;
Alan Viverette058ac7c2013-08-19 16:44:30 -070037import android.view.accessibility.AccessibilityNodeInfo;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070038import android.widget.ActivityChooserModel.ActivityChooserModelClient;
Alan Viverette5d46c892013-09-03 14:56:26 -070039import android.widget.ListPopupWindow.ForwardingListener;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070040
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070041/**
42 * This class is a view for choosing an activity for handling a given {@link Intent}.
43 * <p>
44 * The view is composed of two adjacent buttons:
45 * <ul>
46 * <li>
47 * The left button is an immediate action and allows one click activity choosing.
48 * Tapping this button immediately executes the intent without requiring any further
49 * user input. Long press on this button shows a popup for changing the default
50 * activity.
51 * </li>
52 * <li>
53 * The right button is an overflow action and provides an optimized menu
54 * of additional activities. Tapping this button shows a popup anchored to this
55 * view, listing the most frequently used activities. This list is initially
56 * limited to a small number of items in frequency used order. The last item,
57 * "Show all..." serves as an affordance to display all available activities.
58 * </li>
59 * </ul>
60 * </p>
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070061 *
62 * @hide
63 */
64public class ActivityChooserView extends ViewGroup implements ActivityChooserModelClient {
65
Svetoslavbaeabb62013-10-28 15:22:14 -070066 private static final String LOG_TAG = "ActivityChooserView";
67
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070068 /**
69 * An adapter for displaying the activities in an {@link AdapterView}.
70 */
71 private final ActivityChooserViewAdapter mAdapter;
72
73 /**
74 * Implementation of various interfaces to avoid publishing them in the APIs.
75 */
76 private final Callbacks mCallbacks;
77
78 /**
79 * The content of this view.
80 */
81 private final LinearLayout mActivityChooserContent;
82
83 /**
Svetoslav Ganovf2e75402011-09-07 16:38:40 -070084 * Stores the background drawable to allow hiding and latter showing.
85 */
86 private final Drawable mActivityChooserContentBackground;
87
88 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070089 * The expand activities action button;
90 */
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -070091 private final FrameLayout mExpandActivityOverflowButton;
92
93 /**
94 * The image for the expand activities action button;
95 */
96 private final ImageView mExpandActivityOverflowButtonImage;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070097
98 /**
99 * The default activities action button;
100 */
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700101 private final FrameLayout mDefaultActivityButton;
102
103 /**
104 * The image for the default activities action button;
105 */
106 private final ImageView mDefaultActivityButtonImage;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700107
108 /**
Svetoslav Ganov414051b2011-07-17 22:28:42 -0700109 * The maximal width of the list popup.
110 */
111 private final int mListPopupMaxWidth;
112
113 /**
Adam Powell823f0742011-09-21 17:17:01 -0700114 * The ActionProvider hosting this view, if applicable.
115 */
116 ActionProvider mProvider;
117
118 /**
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700119 * Observer for the model data.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700120 */
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700121 private final DataSetObserver mModelDataSetOberver = new DataSetObserver() {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700122
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700123 @Override
124 public void onChanged() {
125 super.onChanged();
126 mAdapter.notifyDataSetChanged();
127 }
128 @Override
129 public void onInvalidated() {
130 super.onInvalidated();
131 mAdapter.notifyDataSetInvalidated();
132 }
133 };
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700134
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700135 private final OnGlobalLayoutListener mOnGlobalLayoutListener = new OnGlobalLayoutListener() {
136 @Override
137 public void onGlobalLayout() {
138 if (isShowingPopup()) {
139 if (!isShown()) {
140 getListPopupWindow().dismiss();
141 } else {
142 getListPopupWindow().show();
Adam Powell823f0742011-09-21 17:17:01 -0700143 if (mProvider != null) {
144 mProvider.subUiVisibilityChanged(true);
145 }
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700146 }
147 }
148 }
149 };
150
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700151 /**
152 * Popup window for showing the activity overflow list.
153 */
154 private ListPopupWindow mListPopupWindow;
155
156 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700157 * Listener for the dismissal of the popup/alert.
158 */
159 private PopupWindow.OnDismissListener mOnDismissListener;
160
161 /**
162 * Flag whether a default activity currently being selected.
163 */
164 private boolean mIsSelectingDefaultActivity;
165
166 /**
167 * The count of activities in the popup.
168 */
169 private int mInitialActivityCount = ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT;
170
171 /**
172 * Flag whether this view is attached to a window.
173 */
174 private boolean mIsAttachedToWindow;
175
176 /**
Svetoslav Ganov70853772011-09-30 19:57:35 -0700177 * String resource for formatting content description of the default target.
178 */
179 private int mDefaultActionButtonContentDescription;
180
181 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700182 * Create a new instance.
183 *
184 * @param context The application environment.
185 */
186 public ActivityChooserView(Context context) {
187 this(context, null);
188 }
189
190 /**
191 * Create a new instance.
192 *
193 * @param context The application environment.
194 * @param attrs A collection of attributes.
195 */
196 public ActivityChooserView(Context context, AttributeSet attrs) {
Adam Powell23f4cc02011-08-18 10:30:46 -0700197 this(context, attrs, 0);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700198 }
199
200 /**
201 * Create a new instance.
202 *
203 * @param context The application environment.
204 * @param attrs A collection of attributes.
Alan Viverette617feb92013-09-09 18:09:13 -0700205 * @param defStyleAttr An attribute in the current theme that contains a
206 * reference to a style resource that supplies default values for
207 * the view. Can be 0 to not look for defaults.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700208 */
Alan Viverette617feb92013-09-09 18:09:13 -0700209 public ActivityChooserView(Context context, AttributeSet attrs, int defStyleAttr) {
210 this(context, attrs, defStyleAttr, 0);
211 }
212
213 /**
214 * Create a new instance.
215 *
216 * @param context The application environment.
217 * @param attrs A collection of attributes.
218 * @param defStyleAttr An attribute in the current theme that contains a
219 * reference to a style resource that supplies default values for
220 * the view. Can be 0 to not look for defaults.
221 * @param defStyleRes A resource identifier of a style resource that
222 * supplies default values for the view, used only if
223 * defStyleAttr is 0 or can not be found in the theme. Can be 0
224 * to not look for defaults.
225 */
226 public ActivityChooserView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
227 super(context, attrs, defStyleAttr, defStyleRes);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700228
229 TypedArray attributesArray = context.obtainStyledAttributes(attrs,
Alan Viverette617feb92013-09-09 18:09:13 -0700230 R.styleable.ActivityChooserView, defStyleAttr, defStyleRes);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700231
232 mInitialActivityCount = attributesArray.getInt(
233 R.styleable.ActivityChooserView_initialActivityCount,
234 ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT);
235
236 Drawable expandActivityOverflowButtonDrawable = attributesArray.getDrawable(
237 R.styleable.ActivityChooserView_expandActivityOverflowButtonDrawable);
238
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700239 attributesArray.recycle();
240
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700241 LayoutInflater inflater = LayoutInflater.from(mContext);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700242 inflater.inflate(R.layout.activity_chooser_view, this, true);
243
244 mCallbacks = new Callbacks();
245
246 mActivityChooserContent = (LinearLayout) findViewById(R.id.activity_chooser_view_content);
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700247 mActivityChooserContentBackground = mActivityChooserContent.getBackground();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700248
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700249 mDefaultActivityButton = (FrameLayout) findViewById(R.id.default_activity_button);
250 mDefaultActivityButton.setOnClickListener(mCallbacks);
251 mDefaultActivityButton.setOnLongClickListener(mCallbacks);
252 mDefaultActivityButtonImage = (ImageView) mDefaultActivityButton.findViewById(R.id.image);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700253
Alan Viverette5d46c892013-09-03 14:56:26 -0700254 final FrameLayout expandButton = (FrameLayout) findViewById(R.id.expand_activities_button);
255 expandButton.setOnClickListener(mCallbacks);
256 expandButton.setAccessibilityDelegate(new AccessibilityDelegate() {
Alan Viverette058ac7c2013-08-19 16:44:30 -0700257 @Override
258 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
259 super.onInitializeAccessibilityNodeInfo(host, info);
Svetoslav Ganovcb8ed392013-08-23 20:37:28 -0700260 info.setCanOpenPopup(true);
Alan Viverette058ac7c2013-08-19 16:44:30 -0700261 }
262 });
Alan Viverette5d46c892013-09-03 14:56:26 -0700263 expandButton.setOnTouchListener(new ForwardingListener(expandButton) {
264 @Override
265 public ListPopupWindow getPopup() {
266 return getListPopupWindow();
267 }
268
269 @Override
270 protected boolean onForwardingStarted() {
271 showPopup();
272 return true;
273 }
274
275 @Override
276 protected boolean onForwardingStopped() {
277 dismissPopup();
278 return true;
279 }
280 });
281 mExpandActivityOverflowButton = expandButton;
282
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700283 mExpandActivityOverflowButtonImage =
Alan Viverette5d46c892013-09-03 14:56:26 -0700284 (ImageView) expandButton.findViewById(R.id.image);
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700285 mExpandActivityOverflowButtonImage.setImageDrawable(expandActivityOverflowButtonDrawable);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700286
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700287 mAdapter = new ActivityChooserViewAdapter();
288 mAdapter.registerDataSetObserver(new DataSetObserver() {
289 @Override
290 public void onChanged() {
291 super.onChanged();
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700292 updateAppearance();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700293 }
294 });
Svetoslav Ganov414051b2011-07-17 22:28:42 -0700295
296 Resources resources = context.getResources();
297 mListPopupMaxWidth = Math.max(resources.getDisplayMetrics().widthPixels / 2,
298 resources.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700299 }
300
301 /**
302 * {@inheritDoc}
303 */
304 public void setActivityChooserModel(ActivityChooserModel dataModel) {
305 mAdapter.setDataModel(dataModel);
306 if (isShowingPopup()) {
307 dismissPopup();
308 showPopup();
309 }
310 }
311
312 /**
313 * Sets the background for the button that expands the activity
314 * overflow list.
315 *
316 * <strong>Note:</strong> Clients would like to set this drawable
317 * as a clue about the action the chosen activity will perform. For
Svetoslav Ganov70853772011-09-30 19:57:35 -0700318 * example, if a share activity is to be chosen the drawable should
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700319 * give a clue that sharing is to be performed.
320 *
321 * @param drawable The drawable.
322 */
323 public void setExpandActivityOverflowButtonDrawable(Drawable drawable) {
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700324 mExpandActivityOverflowButtonImage.setImageDrawable(drawable);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700325 }
326
327 /**
Svetoslav Ganov70853772011-09-30 19:57:35 -0700328 * Sets the content description for the button that expands the activity
329 * overflow list.
330 *
331 * description as a clue about the action performed by the button.
332 * For example, if a share activity is to be chosen the content
333 * description should be something like "Share with".
334 *
335 * @param resourceId The content description resource id.
336 */
337 public void setExpandActivityOverflowButtonContentDescription(int resourceId) {
338 CharSequence contentDescription = mContext.getString(resourceId);
339 mExpandActivityOverflowButtonImage.setContentDescription(contentDescription);
340 }
341
342 /**
Adam Powell823f0742011-09-21 17:17:01 -0700343 * Set the provider hosting this view, if applicable.
344 * @hide Internal use only
345 */
346 public void setProvider(ActionProvider provider) {
347 mProvider = provider;
348 }
349
350 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700351 * Shows the popup window with activities.
352 *
353 * @return True if the popup was shown, false if already showing.
354 */
355 public boolean showPopup() {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700356 if (isShowingPopup() || !mIsAttachedToWindow) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700357 return false;
358 }
359 mIsSelectingDefaultActivity = false;
360 showPopupUnchecked(mInitialActivityCount);
361 return true;
362 }
363
364 /**
365 * Shows the popup no matter if it was already showing.
366 *
367 * @param maxActivityCount The max number of activities to display.
368 */
369 private void showPopupUnchecked(int maxActivityCount) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700370 if (mAdapter.getDataModel() == null) {
371 throw new IllegalStateException("No data model. Did you call #setDataModel?");
372 }
373
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700374 getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
375
Svetoslav Ganovf632eaa2011-09-22 16:05:12 -0700376 final boolean defaultActivityButtonShown =
377 mDefaultActivityButton.getVisibility() == VISIBLE;
378
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700379 final int activityCount = mAdapter.getActivityCount();
Svetoslav Ganovf632eaa2011-09-22 16:05:12 -0700380 final int maxActivityCountOffset = defaultActivityButtonShown ? 1 : 0;
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700381 if (maxActivityCount != ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED
Svetoslav Ganovf632eaa2011-09-22 16:05:12 -0700382 && activityCount > maxActivityCount + maxActivityCountOffset) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700383 mAdapter.setShowFooterView(true);
Svetoslav Ganov1c07e222011-09-29 16:50:59 -0700384 mAdapter.setMaxActivityCount(maxActivityCount - 1);
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700385 } else {
386 mAdapter.setShowFooterView(false);
Svetoslav Ganov1c07e222011-09-29 16:50:59 -0700387 mAdapter.setMaxActivityCount(maxActivityCount);
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700388 }
389
390 ListPopupWindow popupWindow = getListPopupWindow();
391 if (!popupWindow.isShowing()) {
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700392 if (mIsSelectingDefaultActivity || !defaultActivityButtonShown) {
393 mAdapter.setShowDefaultActivity(true, defaultActivityButtonShown);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700394 } else {
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700395 mAdapter.setShowDefaultActivity(false, false);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700396 }
Svetoslav Ganov414051b2011-07-17 22:28:42 -0700397 final int contentWidth = Math.min(mAdapter.measureContentWidth(), mListPopupMaxWidth);
398 popupWindow.setContentWidth(contentWidth);
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700399 popupWindow.show();
Adam Powell823f0742011-09-21 17:17:01 -0700400 if (mProvider != null) {
401 mProvider.subUiVisibilityChanged(true);
402 }
Svetoslav Ganov70853772011-09-30 19:57:35 -0700403 popupWindow.getListView().setContentDescription(mContext.getString(
404 R.string.activitychooserview_choose_application));
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700405 }
406 }
407
408 /**
409 * Dismisses the popup window with activities.
410 *
411 * @return True if dismissed, false if already dismissed.
412 */
413 public boolean dismissPopup() {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700414 if (isShowingPopup()) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700415 getListPopupWindow().dismiss();
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700416 ViewTreeObserver viewTreeObserver = getViewTreeObserver();
417 if (viewTreeObserver.isAlive()) {
Svetoslav Ganovaa47a6b2012-01-24 13:55:35 -0800418 viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700419 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700420 }
421 return true;
422 }
423
424 /**
425 * Gets whether the popup window with activities is shown.
426 *
427 * @return True if the popup is shown.
428 */
429 public boolean isShowingPopup() {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700430 return getListPopupWindow().isShowing();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700431 }
432
433 @Override
434 protected void onAttachedToWindow() {
435 super.onAttachedToWindow();
436 ActivityChooserModel dataModel = mAdapter.getDataModel();
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700437 if (dataModel != null) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700438 dataModel.registerObserver(mModelDataSetOberver);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700439 }
440 mIsAttachedToWindow = true;
441 }
442
443 @Override
444 protected void onDetachedFromWindow() {
445 super.onDetachedFromWindow();
446 ActivityChooserModel dataModel = mAdapter.getDataModel();
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700447 if (dataModel != null) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700448 dataModel.unregisterObserver(mModelDataSetOberver);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700449 }
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700450 ViewTreeObserver viewTreeObserver = getViewTreeObserver();
451 if (viewTreeObserver.isAlive()) {
Svetoslav Ganovaa47a6b2012-01-24 13:55:35 -0800452 viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700453 }
Svetoslav Ganov748ee2e2012-05-23 15:21:10 -0700454 if (isShowingPopup()) {
455 dismissPopup();
456 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700457 mIsAttachedToWindow = false;
458 }
459
460 @Override
461 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700462 View child = mActivityChooserContent;
463 // If the default action is not visible we want to be as tall as the
464 // ActionBar so if this widget is used in the latter it will look as
465 // a normal action button.
466 if (mDefaultActivityButton.getVisibility() != VISIBLE) {
467 heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
468 MeasureSpec.EXACTLY);
469 }
470 measureChild(child, widthMeasureSpec, heightMeasureSpec);
471 setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight());
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700472 }
473
474 @Override
475 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700476 mActivityChooserContent.layout(0, 0, right - left, bottom - top);
Svetoslav Ganov748ee2e2012-05-23 15:21:10 -0700477 if (!isShowingPopup()) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700478 dismissPopup();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700479 }
480 }
481
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700482 public ActivityChooserModel getDataModel() {
483 return mAdapter.getDataModel();
484 }
485
486 /**
487 * Sets a listener to receive a callback when the popup is dismissed.
488 *
489 * @param listener The listener to be notified.
490 */
491 public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
492 mOnDismissListener = listener;
493 }
494
495 /**
496 * Sets the initial count of items shown in the activities popup
497 * i.e. the items before the popup is expanded. This is an upper
498 * bound since it is not guaranteed that such number of intent
499 * handlers exist.
500 *
501 * @param itemCount The initial popup item count.
502 */
503 public void setInitialActivityCount(int itemCount) {
504 mInitialActivityCount = itemCount;
505 }
506
507 /**
Svetoslav Ganov70853772011-09-30 19:57:35 -0700508 * Sets a content description of the default action button. This
509 * resource should be a string taking one formatting argument and
510 * will be used for formatting the content description of the button
511 * dynamically as the default target changes. For example, a resource
512 * pointing to the string "share with %1$s" will result in a content
513 * description "share with Bluetooth" for the Bluetooth activity.
514 *
515 * @param resourceId The resource id.
516 */
517 public void setDefaultActionButtonContentDescription(int resourceId) {
518 mDefaultActionButtonContentDescription = resourceId;
519 }
520
521 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700522 * Gets the list popup window which is lazily initialized.
523 *
524 * @return The popup.
525 */
526 private ListPopupWindow getListPopupWindow() {
527 if (mListPopupWindow == null) {
528 mListPopupWindow = new ListPopupWindow(getContext());
529 mListPopupWindow.setAdapter(mAdapter);
530 mListPopupWindow.setAnchorView(ActivityChooserView.this);
531 mListPopupWindow.setModal(true);
532 mListPopupWindow.setOnItemClickListener(mCallbacks);
533 mListPopupWindow.setOnDismissListener(mCallbacks);
534 }
535 return mListPopupWindow;
536 }
537
538 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700539 * Updates the buttons state.
540 */
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700541 private void updateAppearance() {
542 // Expand overflow button.
543 if (mAdapter.getCount() > 0) {
544 mExpandActivityOverflowButton.setEnabled(true);
545 } else {
546 mExpandActivityOverflowButton.setEnabled(false);
547 }
548 // Default activity button.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700549 final int activityCount = mAdapter.getActivityCount();
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700550 final int historySize = mAdapter.getHistorySize();
fredc5a1195f2012-05-15 09:03:56 -0700551 if (activityCount==1 || activityCount > 1 && historySize > 0) {
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700552 mDefaultActivityButton.setVisibility(VISIBLE);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700553 ResolveInfo activity = mAdapter.getDefaultActivity();
554 PackageManager packageManager = mContext.getPackageManager();
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700555 mDefaultActivityButtonImage.setImageDrawable(activity.loadIcon(packageManager));
Svetoslav Ganov70853772011-09-30 19:57:35 -0700556 if (mDefaultActionButtonContentDescription != 0) {
557 CharSequence label = activity.loadLabel(packageManager);
558 String contentDescription = mContext.getString(
559 mDefaultActionButtonContentDescription, label);
560 mDefaultActivityButton.setContentDescription(contentDescription);
561 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700562 } else {
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700563 mDefaultActivityButton.setVisibility(View.GONE);
564 }
565 // Activity chooser content.
566 if (mDefaultActivityButton.getVisibility() == VISIBLE) {
Svetoslavbaeabb62013-10-28 15:22:14 -0700567 mActivityChooserContent.setBackground(mActivityChooserContentBackground);
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700568 } else {
Svetoslavbaeabb62013-10-28 15:22:14 -0700569 mActivityChooserContent.setBackground(null);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700570 }
571 }
572
573 /**
574 * Interface implementation to avoid publishing them in the APIs.
575 */
576 private class Callbacks implements AdapterView.OnItemClickListener,
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700577 View.OnClickListener, View.OnLongClickListener, PopupWindow.OnDismissListener {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700578
579 // AdapterView#OnItemClickListener
580 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
581 ActivityChooserViewAdapter adapter = (ActivityChooserViewAdapter) parent.getAdapter();
582 final int itemViewType = adapter.getItemViewType(position);
583 switch (itemViewType) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700584 case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_FOOTER: {
585 showPopupUnchecked(ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED);
586 } break;
587 case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_ACTIVITY: {
588 dismissPopup();
589 if (mIsSelectingDefaultActivity) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700590 // The item at position zero is the default already.
591 if (position > 0) {
592 mAdapter.getDataModel().setDefaultActivity(position);
593 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700594 } else {
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700595 // If the default target is not shown in the list, the first
596 // item in the model is default action => adjust index
597 position = mAdapter.getShowDefaultActivity() ? position : position + 1;
598 Intent launchIntent = mAdapter.getDataModel().chooseActivity(position);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700599 if (launchIntent != null) {
Adam Powell314419c2012-01-24 13:33:09 -0800600 launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Svetoslavbaeabb62013-10-28 15:22:14 -0700601 ResolveInfo resolveInfo = mAdapter.getDataModel().getActivity(position);
602 startActivity(launchIntent, resolveInfo);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700603 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700604 }
605 } break;
606 default:
607 throw new IllegalArgumentException();
608 }
609 }
610
611 // View.OnClickListener
612 public void onClick(View view) {
Svetoslav Ganovbfbbcf52011-07-25 21:54:55 -0700613 if (view == mDefaultActivityButton) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700614 dismissPopup();
615 ResolveInfo defaultActivity = mAdapter.getDefaultActivity();
616 final int index = mAdapter.getDataModel().getActivityIndex(defaultActivity);
617 Intent launchIntent = mAdapter.getDataModel().chooseActivity(index);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700618 if (launchIntent != null) {
Adam Powell314419c2012-01-24 13:33:09 -0800619 launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Svetoslavbaeabb62013-10-28 15:22:14 -0700620 startActivity(launchIntent, defaultActivity);
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700621 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700622 } else if (view == mExpandActivityOverflowButton) {
623 mIsSelectingDefaultActivity = false;
624 showPopupUnchecked(mInitialActivityCount);
625 } else {
626 throw new IllegalArgumentException();
627 }
628 }
629
630 // OnLongClickListener#onLongClick
631 @Override
632 public boolean onLongClick(View view) {
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700633 if (view == mDefaultActivityButton) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700634 if (mAdapter.getCount() > 0) {
635 mIsSelectingDefaultActivity = true;
636 showPopupUnchecked(mInitialActivityCount);
637 }
638 } else {
639 throw new IllegalArgumentException();
640 }
641 return true;
642 }
643
644 // PopUpWindow.OnDismissListener#onDismiss
645 public void onDismiss() {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700646 notifyOnDismissListener();
Adam Powell823f0742011-09-21 17:17:01 -0700647 if (mProvider != null) {
648 mProvider.subUiVisibilityChanged(false);
649 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700650 }
651
652 private void notifyOnDismissListener() {
653 if (mOnDismissListener != null) {
654 mOnDismissListener.onDismiss();
655 }
656 }
Svetoslavbaeabb62013-10-28 15:22:14 -0700657
658 private void startActivity(Intent intent, ResolveInfo resolveInfo) {
659 try {
660 mContext.startActivity(intent);
661 } catch (RuntimeException re) {
662 CharSequence appLabel = resolveInfo.loadLabel(mContext.getPackageManager());
663 String message = mContext.getString(
664 R.string.activitychooserview_choose_application_error, appLabel);
665 Log.e(LOG_TAG, message);
666 Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
667 }
668 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700669 }
670
671 /**
672 * Adapter for backing the list of activities shown in the popup.
673 */
674 private class ActivityChooserViewAdapter extends BaseAdapter {
675
676 public static final int MAX_ACTIVITY_COUNT_UNLIMITED = Integer.MAX_VALUE;
677
678 public static final int MAX_ACTIVITY_COUNT_DEFAULT = 4;
679
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700680 private static final int ITEM_VIEW_TYPE_ACTIVITY = 0;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700681
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700682 private static final int ITEM_VIEW_TYPE_FOOTER = 1;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700683
684 private static final int ITEM_VIEW_TYPE_COUNT = 3;
685
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700686 private ActivityChooserModel mDataModel;
687
688 private int mMaxActivityCount = MAX_ACTIVITY_COUNT_DEFAULT;
689
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700690 private boolean mShowDefaultActivity;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700691
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700692 private boolean mHighlightDefaultActivity;
693
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700694 private boolean mShowFooterView;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700695
696 public void setDataModel(ActivityChooserModel dataModel) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700697 ActivityChooserModel oldDataModel = mAdapter.getDataModel();
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700698 if (oldDataModel != null && isShown()) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700699 oldDataModel.unregisterObserver(mModelDataSetOberver);
700 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700701 mDataModel = dataModel;
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700702 if (dataModel != null && isShown()) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700703 dataModel.registerObserver(mModelDataSetOberver);
704 }
Svetoslav Ganovb9f286e2012-05-14 23:09:02 -0700705 notifyDataSetChanged();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700706 }
707
708 @Override
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700709 public int getItemViewType(int position) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700710 if (mShowFooterView && position == getCount() - 1) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700711 return ITEM_VIEW_TYPE_FOOTER;
712 } else {
713 return ITEM_VIEW_TYPE_ACTIVITY;
714 }
715 }
716
717 @Override
718 public int getViewTypeCount() {
719 return ITEM_VIEW_TYPE_COUNT;
720 }
721
722 public int getCount() {
723 int count = 0;
724 int activityCount = mDataModel.getActivityCount();
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700725 if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700726 activityCount--;
727 }
728 count = Math.min(activityCount, mMaxActivityCount);
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700729 if (mShowFooterView) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700730 count++;
731 }
732 return count;
733 }
734
735 public Object getItem(int position) {
736 final int itemViewType = getItemViewType(position);
737 switch (itemViewType) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700738 case ITEM_VIEW_TYPE_FOOTER:
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700739 return null;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700740 case ITEM_VIEW_TYPE_ACTIVITY:
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700741 if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) {
742 position++;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700743 }
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700744 return mDataModel.getActivity(position);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700745 default:
746 throw new IllegalArgumentException();
747 }
748 }
749
750 public long getItemId(int position) {
751 return position;
752 }
753
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700754 public View getView(int position, View convertView, ViewGroup parent) {
755 final int itemViewType = getItemViewType(position);
756 switch (itemViewType) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700757 case ITEM_VIEW_TYPE_FOOTER:
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700758 if (convertView == null || convertView.getId() != ITEM_VIEW_TYPE_FOOTER) {
759 convertView = LayoutInflater.from(getContext()).inflate(
760 R.layout.activity_chooser_view_list_item, parent, false);
761 convertView.setId(ITEM_VIEW_TYPE_FOOTER);
762 TextView titleView = (TextView) convertView.findViewById(R.id.title);
763 titleView.setText(mContext.getString(
764 R.string.activity_chooser_view_see_all));
765 }
766 return convertView;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700767 case ITEM_VIEW_TYPE_ACTIVITY:
768 if (convertView == null || convertView.getId() != R.id.list_item) {
769 convertView = LayoutInflater.from(getContext()).inflate(
770 R.layout.activity_chooser_view_list_item, parent, false);
771 }
772 PackageManager packageManager = mContext.getPackageManager();
773 // Set the icon
774 ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
775 ResolveInfo activity = (ResolveInfo) getItem(position);
Svetoslav Ganov414051b2011-07-17 22:28:42 -0700776 iconView.setImageDrawable(activity.loadIcon(packageManager));
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700777 // Set the title.
778 TextView titleView = (TextView) convertView.findViewById(R.id.title);
779 titleView.setText(activity.loadLabel(packageManager));
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700780 // Highlight the default.
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700781 if (mShowDefaultActivity && position == 0 && mHighlightDefaultActivity) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700782 convertView.setActivated(true);
783 } else {
784 convertView.setActivated(false);
785 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700786 return convertView;
787 default:
788 throw new IllegalArgumentException();
789 }
790 }
791
792 public int measureContentWidth() {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700793 // The user may have specified some of the target not to be shown but we
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700794 // want to measure all of them since after expansion they should fit.
795 final int oldMaxActivityCount = mMaxActivityCount;
796 mMaxActivityCount = MAX_ACTIVITY_COUNT_UNLIMITED;
797
798 int contentWidth = 0;
799 View itemView = null;
800
801 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
802 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
803 final int count = getCount();
804
805 for (int i = 0; i < count; i++) {
806 itemView = getView(i, itemView, null);
807 itemView.measure(widthMeasureSpec, heightMeasureSpec);
808 contentWidth = Math.max(contentWidth, itemView.getMeasuredWidth());
809 }
810
811 mMaxActivityCount = oldMaxActivityCount;
812
813 return contentWidth;
814 }
815
816 public void setMaxActivityCount(int maxActivityCount) {
817 if (mMaxActivityCount != maxActivityCount) {
818 mMaxActivityCount = maxActivityCount;
819 notifyDataSetChanged();
820 }
821 }
822
823 public ResolveInfo getDefaultActivity() {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700824 return mDataModel.getDefaultActivity();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700825 }
826
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700827 public void setShowFooterView(boolean showFooterView) {
828 if (mShowFooterView != showFooterView) {
829 mShowFooterView = showFooterView;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700830 notifyDataSetChanged();
831 }
832 }
833
834 public int getActivityCount() {
835 return mDataModel.getActivityCount();
836 }
837
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700838 public int getHistorySize() {
839 return mDataModel.getHistorySize();
840 }
841
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700842 public ActivityChooserModel getDataModel() {
843 return mDataModel;
844 }
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700845
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700846 public void setShowDefaultActivity(boolean showDefaultActivity,
847 boolean highlightDefaultActivity) {
848 if (mShowDefaultActivity != showDefaultActivity
849 || mHighlightDefaultActivity != highlightDefaultActivity) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700850 mShowDefaultActivity = showDefaultActivity;
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700851 mHighlightDefaultActivity = highlightDefaultActivity;
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700852 notifyDataSetChanged();
853 }
854 }
Svetoslav Ganovefab4e72011-09-20 14:35:39 -0700855
856 public boolean getShowDefaultActivity() {
857 return mShowDefaultActivity;
858 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700859 }
860}