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