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