1 /*
2  * Copyright (C) 2013 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 
17 package androidx.appcompat.widget;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.content.res.Resources;
26 import android.content.res.TypedArray;
27 import android.database.DataSetObserver;
28 import android.graphics.Color;
29 import android.graphics.drawable.ColorDrawable;
30 import android.graphics.drawable.Drawable;
31 import android.util.AttributeSet;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.ViewTreeObserver;
36 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
37 import android.view.accessibility.AccessibilityNodeInfo;
38 import android.widget.AdapterView;
39 import android.widget.BaseAdapter;
40 import android.widget.FrameLayout;
41 import android.widget.ImageView;
42 import android.widget.LinearLayout;
43 import android.widget.PopupWindow;
44 import android.widget.TextView;
45 
46 import androidx.annotation.RestrictTo;
47 import androidx.appcompat.R;
48 import androidx.appcompat.view.menu.ShowableListMenu;
49 import androidx.core.view.ActionProvider;
50 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
51 
52 /**
53  * This class is a view for choosing an activity for handling a given {@link Intent}.
54  * <p>
55  * The view is composed of two adjacent buttons:
56  * <ul>
57  * <li>
58  * The left button is an immediate action and allows one click activity choosing.
59  * Tapping this button immediately executes the intent without requiring any further
60  * user input. Long press on this button shows a popup for changing the default
61  * activity.
62  * </li>
63  * <li>
64  * The right button is an overflow action and provides an optimized menu
65  * of additional activities. Tapping this button shows a popup anchored to this
66  * view, listing the most frequently used activities. This list is initially
67  * limited to a small number of items in frequency used order. The last item,
68  * "Show all..." serves as an affordance to display all available activities.
69  * </li>
70  * </ul>
71  * </p>
72  *
73  * @hide
74  */
75 @RestrictTo(LIBRARY_GROUP)
76 public class ActivityChooserView extends ViewGroup implements
77         ActivityChooserModel.ActivityChooserModelClient {
78 
79     private static final String LOG_TAG = "ActivityChooserView";
80 
81     /**
82      * An adapter for displaying the activities in an {@link android.widget.AdapterView}.
83      */
84     final ActivityChooserViewAdapter mAdapter;
85 
86     /**
87      * Implementation of various interfaces to avoid publishing them in the APIs.
88      */
89     private final Callbacks mCallbacks;
90 
91     /**
92      * The content of this view.
93      */
94     private final View mActivityChooserContent;
95 
96     /**
97      * Stores the background drawable to allow hiding and latter showing.
98      */
99     private final Drawable mActivityChooserContentBackground;
100 
101     /**
102      * The expand activities action button;
103      */
104     final FrameLayout mExpandActivityOverflowButton;
105 
106     /**
107      * The image for the expand activities action button;
108      */
109     private final ImageView mExpandActivityOverflowButtonImage;
110 
111     /**
112      * The default activities action button;
113      */
114     final FrameLayout mDefaultActivityButton;
115 
116     /**
117      * The image for the default activities action button;
118      */
119     private final ImageView mDefaultActivityButtonImage;
120 
121     /**
122      * The maximal width of the list popup.
123      */
124     private final int mListPopupMaxWidth;
125 
126     /**
127      * The ActionProvider hosting this view, if applicable.
128      */
129     ActionProvider mProvider;
130 
131     /**
132      * Observer for the model data.
133      */
134     final DataSetObserver mModelDataSetObserver = new DataSetObserver() {
135 
136         @Override
137         public void onChanged() {
138             super.onChanged();
139             mAdapter.notifyDataSetChanged();
140         }
141         @Override
142         public void onInvalidated() {
143             super.onInvalidated();
144             mAdapter.notifyDataSetInvalidated();
145         }
146     };
147 
148     private final OnGlobalLayoutListener mOnGlobalLayoutListener = new OnGlobalLayoutListener() {
149         @Override
150         public void onGlobalLayout() {
151             if (isShowingPopup()) {
152                 if (!isShown()) {
153                     getListPopupWindow().dismiss();
154                 } else {
155                     getListPopupWindow().show();
156                     if (mProvider != null) {
157                         mProvider.subUiVisibilityChanged(true);
158                     }
159                 }
160             }
161         }
162     };
163 
164     /**
165      * Popup window for showing the activity overflow list.
166      */
167     private ListPopupWindow mListPopupWindow;
168 
169     /**
170      * Listener for the dismissal of the popup/alert.
171      */
172     PopupWindow.OnDismissListener mOnDismissListener;
173 
174     /**
175      * Flag whether a default activity currently being selected.
176      */
177     boolean mIsSelectingDefaultActivity;
178 
179     /**
180      * The count of activities in the popup.
181      */
182     int mInitialActivityCount = ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT;
183 
184     /**
185      * Flag whether this view is attached to a window.
186      */
187     private boolean mIsAttachedToWindow;
188 
189     /**
190      * String resource for formatting content description of the default target.
191      */
192     private int mDefaultActionButtonContentDescription;
193 
194     /**
195      * Create a new instance.
196      *
197      * @param context The application environment.
198      */
ActivityChooserView(Context context)199     public ActivityChooserView(Context context) {
200         this(context, null);
201     }
202 
203     /**
204      * Create a new instance.
205      *
206      * @param context The application environment.
207      * @param attrs A collection of attributes.
208      */
ActivityChooserView(Context context, AttributeSet attrs)209     public ActivityChooserView(Context context, AttributeSet attrs) {
210         this(context, attrs, 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 defStyle The default style to apply to this view.
219      */
ActivityChooserView(Context context, AttributeSet attrs, int defStyle)220     public ActivityChooserView(Context context, AttributeSet attrs, int defStyle) {
221         super(context, attrs, defStyle);
222 
223         TypedArray attributesArray = context.obtainStyledAttributes(attrs,
224                 R.styleable.ActivityChooserView, defStyle, 0);
225 
226         mInitialActivityCount = attributesArray.getInt(
227                 R.styleable.ActivityChooserView_initialActivityCount,
228                 ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT);
229 
230         Drawable expandActivityOverflowButtonDrawable = attributesArray.getDrawable(
231                 R.styleable.ActivityChooserView_expandActivityOverflowButtonDrawable);
232 
233         attributesArray.recycle();
234 
235         LayoutInflater inflater = LayoutInflater.from(getContext());
236         inflater.inflate(R.layout.abc_activity_chooser_view, this, true);
237 
238         mCallbacks = new Callbacks();
239 
240         mActivityChooserContent = findViewById(R.id.activity_chooser_view_content);
241         mActivityChooserContentBackground = mActivityChooserContent.getBackground();
242 
243         mDefaultActivityButton = findViewById(R.id.default_activity_button);
244         mDefaultActivityButton.setOnClickListener(mCallbacks);
245         mDefaultActivityButton.setOnLongClickListener(mCallbacks);
246         mDefaultActivityButtonImage = (ImageView) mDefaultActivityButton.findViewById(R.id.image);
247 
248         final FrameLayout expandButton = findViewById(R.id.expand_activities_button);
249         expandButton.setOnClickListener(mCallbacks);
250         expandButton.setAccessibilityDelegate(new AccessibilityDelegate() {
251             @Override
252             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
253                 super.onInitializeAccessibilityNodeInfo(host, info);
254                 AccessibilityNodeInfoCompat.wrap(info).setCanOpenPopup(true);
255             }
256         });
257         expandButton.setOnTouchListener(new ForwardingListener(expandButton) {
258             @Override
259             public ShowableListMenu getPopup() {
260                 return getListPopupWindow();
261             }
262 
263             @Override
264             protected boolean onForwardingStarted() {
265                 showPopup();
266                 return true;
267             }
268 
269             @Override
270             protected boolean onForwardingStopped() {
271                 dismissPopup();
272                 return true;
273             }
274         });
275         mExpandActivityOverflowButton = expandButton;
276         mExpandActivityOverflowButtonImage =
277             (ImageView) expandButton.findViewById(R.id.image);
278         mExpandActivityOverflowButtonImage.setImageDrawable(expandActivityOverflowButtonDrawable);
279 
280         mAdapter = new ActivityChooserViewAdapter();
281         mAdapter.registerDataSetObserver(new DataSetObserver() {
282             @Override
283             public void onChanged() {
284                 super.onChanged();
285                 updateAppearance();
286             }
287         });
288 
289         Resources resources = context.getResources();
290         mListPopupMaxWidth = Math.max(resources.getDisplayMetrics().widthPixels / 2,
291                 resources.getDimensionPixelSize(R.dimen.abc_config_prefDialogWidth));
292     }
293 
294     /**
295      * {@inheritDoc}
296      */
297     @Override
setActivityChooserModel(ActivityChooserModel dataModel)298     public void setActivityChooserModel(ActivityChooserModel dataModel) {
299         mAdapter.setDataModel(dataModel);
300         if (isShowingPopup()) {
301             dismissPopup();
302             showPopup();
303         }
304     }
305 
306     /**
307      * Sets the background for the button that expands the activity
308      * overflow list.
309      *
310      * <strong>Note:</strong> Clients would like to set this drawable
311      * as a clue about the action the chosen activity will perform. For
312      * example, if a share activity is to be chosen the drawable should
313      * give a clue that sharing is to be performed.
314      *
315      * @param drawable The drawable.
316      */
setExpandActivityOverflowButtonDrawable(Drawable drawable)317     public void setExpandActivityOverflowButtonDrawable(Drawable drawable) {
318         mExpandActivityOverflowButtonImage.setImageDrawable(drawable);
319     }
320 
321     /**
322      * Sets the content description for the button that expands the activity
323      * overflow list.
324      *
325      * description as a clue about the action performed by the button.
326      * For example, if a share activity is to be chosen the content
327      * description should be something like "Share with".
328      *
329      * @param resourceId The content description resource id.
330      */
setExpandActivityOverflowButtonContentDescription(int resourceId)331     public void setExpandActivityOverflowButtonContentDescription(int resourceId) {
332         CharSequence contentDescription = getContext().getString(resourceId);
333         mExpandActivityOverflowButtonImage.setContentDescription(contentDescription);
334     }
335 
336     /**
337      * Set the provider hosting this view, if applicable.
338      * @hide Internal use only
339      */
340     @RestrictTo(LIBRARY_GROUP)
setProvider(ActionProvider provider)341     public void setProvider(ActionProvider provider) {
342         mProvider = provider;
343     }
344 
345     /**
346      * Shows the popup window with activities.
347      *
348      * @return True if the popup was shown, false if already showing.
349      */
showPopup()350     public boolean showPopup() {
351         if (isShowingPopup() || !mIsAttachedToWindow) {
352             return false;
353         }
354         mIsSelectingDefaultActivity = false;
355         showPopupUnchecked(mInitialActivityCount);
356         return true;
357     }
358 
359     /**
360      * Shows the popup no matter if it was already showing.
361      *
362      * @param maxActivityCount The max number of activities to display.
363      */
showPopupUnchecked(int maxActivityCount)364     void showPopupUnchecked(int maxActivityCount) {
365         if (mAdapter.getDataModel() == null) {
366             throw new IllegalStateException("No data model. Did you call #setDataModel?");
367         }
368 
369         getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
370 
371         final boolean defaultActivityButtonShown =
372                 mDefaultActivityButton.getVisibility() == VISIBLE;
373 
374         final int activityCount = mAdapter.getActivityCount();
375         final int maxActivityCountOffset = defaultActivityButtonShown ? 1 : 0;
376         if (maxActivityCount != ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED
377                 && activityCount > maxActivityCount + maxActivityCountOffset) {
378             mAdapter.setShowFooterView(true);
379             mAdapter.setMaxActivityCount(maxActivityCount - 1);
380         } else {
381             mAdapter.setShowFooterView(false);
382             mAdapter.setMaxActivityCount(maxActivityCount);
383         }
384 
385         ListPopupWindow popupWindow = getListPopupWindow();
386         if (!popupWindow.isShowing()) {
387             if (mIsSelectingDefaultActivity || !defaultActivityButtonShown) {
388                 mAdapter.setShowDefaultActivity(true, defaultActivityButtonShown);
389             } else {
390                 mAdapter.setShowDefaultActivity(false, false);
391             }
392             final int contentWidth = Math.min(mAdapter.measureContentWidth(), mListPopupMaxWidth);
393             popupWindow.setContentWidth(contentWidth);
394             popupWindow.show();
395             if (mProvider != null) {
396                 mProvider.subUiVisibilityChanged(true);
397             }
398             popupWindow.getListView().setContentDescription(getContext().getString(
399                     R.string.abc_activitychooserview_choose_application));
400             popupWindow.getListView().setSelector(new ColorDrawable(Color.TRANSPARENT));
401         }
402     }
403 
404     /**
405      * Dismisses the popup window with activities.
406      *
407      * @return True if dismissed, false if already dismissed.
408      */
dismissPopup()409     public boolean dismissPopup() {
410         if (isShowingPopup()) {
411             getListPopupWindow().dismiss();
412             ViewTreeObserver viewTreeObserver = getViewTreeObserver();
413             if (viewTreeObserver.isAlive()) {
414                 viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener);
415             }
416         }
417         return true;
418     }
419 
420     /**
421      * Gets whether the popup window with activities is shown.
422      *
423      * @return True if the popup is shown.
424      */
isShowingPopup()425     public boolean isShowingPopup() {
426         return getListPopupWindow().isShowing();
427     }
428 
429     @Override
onAttachedToWindow()430     protected void onAttachedToWindow() {
431         super.onAttachedToWindow();
432         ActivityChooserModel dataModel = mAdapter.getDataModel();
433         if (dataModel != null) {
434             dataModel.registerObserver(mModelDataSetObserver);
435         }
436         mIsAttachedToWindow = true;
437     }
438 
439     @Override
onDetachedFromWindow()440     protected void onDetachedFromWindow() {
441         super.onDetachedFromWindow();
442         ActivityChooserModel dataModel = mAdapter.getDataModel();
443         if (dataModel != null) {
444             dataModel.unregisterObserver(mModelDataSetObserver);
445         }
446         ViewTreeObserver viewTreeObserver = getViewTreeObserver();
447         if (viewTreeObserver.isAlive()) {
448             viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener);
449         }
450         if (isShowingPopup()) {
451             dismissPopup();
452         }
453         mIsAttachedToWindow = false;
454     }
455 
456     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)457     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
458         View child = mActivityChooserContent;
459         // If the default action is not visible we want to be as tall as the
460         // ActionBar so if this widget is used in the latter it will look as
461         // a normal action button.
462         if (mDefaultActivityButton.getVisibility() != VISIBLE) {
463             heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
464                     MeasureSpec.EXACTLY);
465         }
466         measureChild(child, widthMeasureSpec, heightMeasureSpec);
467         setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight());
468     }
469 
470     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)471     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
472         mActivityChooserContent.layout(0, 0, right - left, bottom - top);
473         if (!isShowingPopup()) {
474             dismissPopup();
475         }
476     }
477 
getDataModel()478     public ActivityChooserModel getDataModel() {
479         return mAdapter.getDataModel();
480     }
481 
482     /**
483      * Sets a listener to receive a callback when the popup is dismissed.
484      *
485      * @param listener The listener to be notified.
486      */
setOnDismissListener(PopupWindow.OnDismissListener listener)487     public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
488         mOnDismissListener = listener;
489     }
490 
491     /**
492      * Sets the initial count of items shown in the activities popup
493      * i.e. the items before the popup is expanded. This is an upper
494      * bound since it is not guaranteed that such number of intent
495      * handlers exist.
496      *
497      * @param itemCount The initial popup item count.
498      */
setInitialActivityCount(int itemCount)499     public void setInitialActivityCount(int itemCount) {
500         mInitialActivityCount = itemCount;
501     }
502 
503     /**
504      * Sets a content description of the default action button. This
505      * resource should be a string taking one formatting argument and
506      * will be used for formatting the content description of the button
507      * dynamically as the default target changes. For example, a resource
508      * pointing to the string "share with %1$s" will result in a content
509      * description "share with Bluetooth" for the Bluetooth activity.
510      *
511      * @param resourceId The resource id.
512      */
setDefaultActionButtonContentDescription(int resourceId)513     public void setDefaultActionButtonContentDescription(int resourceId) {
514         mDefaultActionButtonContentDescription = resourceId;
515     }
516 
517     /**
518      * Gets the list popup window which is lazily initialized.
519      *
520      * @return The popup.
521      */
getListPopupWindow()522     ListPopupWindow getListPopupWindow() {
523         if (mListPopupWindow == null) {
524             mListPopupWindow = new ListPopupWindow(getContext());
525             mListPopupWindow.setAdapter(mAdapter);
526             mListPopupWindow.setAnchorView(ActivityChooserView.this);
527             mListPopupWindow.setModal(true);
528             mListPopupWindow.setOnItemClickListener(mCallbacks);
529             mListPopupWindow.setOnDismissListener(mCallbacks);
530         }
531         return mListPopupWindow;
532     }
533 
534     /**
535      * Updates the buttons state.
536      */
updateAppearance()537     void updateAppearance() {
538         // Expand overflow button.
539         if (mAdapter.getCount() > 0) {
540             mExpandActivityOverflowButton.setEnabled(true);
541         } else {
542             mExpandActivityOverflowButton.setEnabled(false);
543         }
544         // Default activity button.
545         final int activityCount = mAdapter.getActivityCount();
546         final int historySize = mAdapter.getHistorySize();
547         if (activityCount == 1 || (activityCount > 1 && historySize > 0)) {
548             mDefaultActivityButton.setVisibility(VISIBLE);
549             ResolveInfo activity = mAdapter.getDefaultActivity();
550             PackageManager packageManager = getContext().getPackageManager();
551             mDefaultActivityButtonImage.setImageDrawable(activity.loadIcon(packageManager));
552             if (mDefaultActionButtonContentDescription != 0) {
553                 CharSequence label = activity.loadLabel(packageManager);
554                 String contentDescription = getContext().getString(
555                         mDefaultActionButtonContentDescription, label);
556                 mDefaultActivityButton.setContentDescription(contentDescription);
557             }
558         } else {
559             mDefaultActivityButton.setVisibility(View.GONE);
560         }
561         // Activity chooser content.
562         if (mDefaultActivityButton.getVisibility() == VISIBLE) {
563             mActivityChooserContent.setBackgroundDrawable(mActivityChooserContentBackground);
564         } else {
565             mActivityChooserContent.setBackgroundDrawable(null);
566         }
567     }
568 
569     /**
570      * Interface implementation to avoid publishing them in the APIs.
571      */
572     private class Callbacks implements AdapterView.OnItemClickListener,
573             View.OnClickListener, View.OnLongClickListener, PopupWindow.OnDismissListener {
574 
Callbacks()575         Callbacks() {
576         }
577 
578         // AdapterView#OnItemClickListener
579         @Override
onItemClick(AdapterView<?> parent, View view, int position, long id)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) {
584                 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) {
590                         // The item at position zero is the default already.
591                         if (position > 0) {
592                             mAdapter.getDataModel().setDefaultActivity(position);
593                         }
594                     } else {
595                         // 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);
599                         if (launchIntent != null) {
600                             launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
601                             getContext().startActivity(launchIntent);
602                         }
603                     }
604                 } break;
605                 default:
606                     throw new IllegalArgumentException();
607             }
608         }
609 
610         // View.OnClickListener
611         @Override
onClick(View view)612         public void onClick(View view) {
613             if (view == mDefaultActivityButton) {
614                 dismissPopup();
615                 ResolveInfo defaultActivity = mAdapter.getDefaultActivity();
616                 final int index = mAdapter.getDataModel().getActivityIndex(defaultActivity);
617                 Intent launchIntent = mAdapter.getDataModel().chooseActivity(index);
618                 if (launchIntent != null) {
619                     launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
620                     getContext().startActivity(launchIntent);
621                 }
622             } else if (view == mExpandActivityOverflowButton) {
623                 mIsSelectingDefaultActivity = false;
624                 showPopupUnchecked(mInitialActivityCount);
625             } else {
626                 throw new IllegalArgumentException();
627             }
628         }
629 
630         // OnLongClickListener#onLongClick
631         @Override
onLongClick(View view)632         public boolean onLongClick(View view) {
633             if (view == mDefaultActivityButton) {
634                 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         @Override
onDismiss()646         public void onDismiss() {
647             notifyOnDismissListener();
648             if (mProvider != null) {
649                 mProvider.subUiVisibilityChanged(false);
650             }
651         }
652 
notifyOnDismissListener()653         private void notifyOnDismissListener() {
654             if (mOnDismissListener != null) {
655                 mOnDismissListener.onDismiss();
656             }
657         }
658     }
659 
660     /**
661      * Adapter for backing the list of activities shown in the popup.
662      */
663     private class ActivityChooserViewAdapter extends BaseAdapter {
664 
665         public static final int MAX_ACTIVITY_COUNT_UNLIMITED = Integer.MAX_VALUE;
666 
667         public static final int MAX_ACTIVITY_COUNT_DEFAULT = 4;
668 
669         private static final int ITEM_VIEW_TYPE_ACTIVITY = 0;
670 
671         private static final int ITEM_VIEW_TYPE_FOOTER = 1;
672 
673         private static final int ITEM_VIEW_TYPE_COUNT = 3;
674 
675         private ActivityChooserModel mDataModel;
676 
677         private int mMaxActivityCount = MAX_ACTIVITY_COUNT_DEFAULT;
678 
679         private boolean mShowDefaultActivity;
680 
681         private boolean mHighlightDefaultActivity;
682 
683         private boolean mShowFooterView;
684 
ActivityChooserViewAdapter()685         ActivityChooserViewAdapter() {
686         }
687 
setDataModel(ActivityChooserModel dataModel)688         public void setDataModel(ActivityChooserModel dataModel) {
689             ActivityChooserModel oldDataModel = mAdapter.getDataModel();
690             if (oldDataModel != null && isShown()) {
691                 oldDataModel.unregisterObserver(mModelDataSetObserver);
692             }
693             mDataModel = dataModel;
694             if (dataModel != null && isShown()) {
695                 dataModel.registerObserver(mModelDataSetObserver);
696             }
697             notifyDataSetChanged();
698         }
699 
700         @Override
getItemViewType(int position)701         public int getItemViewType(int position) {
702             if (mShowFooterView && position == getCount() - 1) {
703                 return ITEM_VIEW_TYPE_FOOTER;
704             } else {
705                 return ITEM_VIEW_TYPE_ACTIVITY;
706             }
707         }
708 
709         @Override
getViewTypeCount()710         public int getViewTypeCount() {
711             return ITEM_VIEW_TYPE_COUNT;
712         }
713 
714         @Override
getCount()715         public int getCount() {
716             int count = 0;
717             int activityCount = mDataModel.getActivityCount();
718             if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) {
719                 activityCount--;
720             }
721             count = Math.min(activityCount, mMaxActivityCount);
722             if (mShowFooterView) {
723                 count++;
724             }
725             return count;
726         }
727 
728         @Override
getItem(int position)729         public Object getItem(int position) {
730             final int itemViewType = getItemViewType(position);
731             switch (itemViewType) {
732                 case ITEM_VIEW_TYPE_FOOTER:
733                     return null;
734                 case ITEM_VIEW_TYPE_ACTIVITY:
735                     if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) {
736                         position++;
737                     }
738                     return mDataModel.getActivity(position);
739                 default:
740                     throw new IllegalArgumentException();
741             }
742         }
743 
744         @Override
getItemId(int position)745         public long getItemId(int position) {
746             return position;
747         }
748 
749         @Override
getView(int position, View convertView, ViewGroup parent)750         public View getView(int position, View convertView, ViewGroup parent) {
751             final int itemViewType = getItemViewType(position);
752             switch (itemViewType) {
753                 case ITEM_VIEW_TYPE_FOOTER:
754                     if (convertView == null || convertView.getId() != ITEM_VIEW_TYPE_FOOTER) {
755                         convertView = LayoutInflater.from(getContext()).inflate(
756                                 R.layout.abc_activity_chooser_view_list_item, parent, false);
757                         convertView.setId(ITEM_VIEW_TYPE_FOOTER);
758                         TextView titleView = (TextView) convertView.findViewById(R.id.title);
759                         titleView.setText(getContext().getString(
760                                 R.string.abc_activity_chooser_view_see_all));
761                     }
762                     return convertView;
763                 case ITEM_VIEW_TYPE_ACTIVITY:
764                     if (convertView == null || convertView.getId() != R.id.list_item) {
765                         convertView = LayoutInflater.from(getContext()).inflate(
766                                 R.layout.abc_activity_chooser_view_list_item, parent, false);
767                     }
768                     PackageManager packageManager = getContext().getPackageManager();
769                     // Set the icon
770                     ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
771                     ResolveInfo activity = (ResolveInfo) getItem(position);
772                     iconView.setImageDrawable(activity.loadIcon(packageManager));
773                     // Set the title.
774                     TextView titleView = (TextView) convertView.findViewById(R.id.title);
775                     titleView.setText(activity.loadLabel(packageManager));
776                     // Highlight the default.
777                     if (mShowDefaultActivity && position == 0 && mHighlightDefaultActivity) {
778                         convertView.setActivated(true);
779                     } else {
780                         convertView.setActivated(false);
781                     }
782                     return convertView;
783                 default:
784                     throw new IllegalArgumentException();
785             }
786         }
787 
measureContentWidth()788         public int measureContentWidth() {
789             // The user may have specified some of the target not to be shown but we
790             // want to measure all of them since after expansion they should fit.
791             final int oldMaxActivityCount = mMaxActivityCount;
792             mMaxActivityCount = MAX_ACTIVITY_COUNT_UNLIMITED;
793 
794             int contentWidth = 0;
795             View itemView = null;
796 
797             final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
798             final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
799             final int count = getCount();
800 
801             for (int i = 0; i < count; i++) {
802                 itemView = getView(i, itemView, null);
803                 itemView.measure(widthMeasureSpec, heightMeasureSpec);
804                 contentWidth = Math.max(contentWidth, itemView.getMeasuredWidth());
805             }
806 
807             mMaxActivityCount = oldMaxActivityCount;
808 
809             return contentWidth;
810         }
811 
setMaxActivityCount(int maxActivityCount)812         public void setMaxActivityCount(int maxActivityCount) {
813             if (mMaxActivityCount != maxActivityCount) {
814                 mMaxActivityCount = maxActivityCount;
815                 notifyDataSetChanged();
816             }
817         }
818 
getDefaultActivity()819         public ResolveInfo getDefaultActivity() {
820             return mDataModel.getDefaultActivity();
821         }
822 
setShowFooterView(boolean showFooterView)823         public void setShowFooterView(boolean showFooterView) {
824             if (mShowFooterView != showFooterView) {
825                 mShowFooterView = showFooterView;
826                 notifyDataSetChanged();
827             }
828         }
829 
getActivityCount()830         public int getActivityCount() {
831             return mDataModel.getActivityCount();
832         }
833 
getHistorySize()834         public int getHistorySize() {
835             return mDataModel.getHistorySize();
836         }
837 
getDataModel()838         public ActivityChooserModel getDataModel() {
839             return mDataModel;
840         }
841 
setShowDefaultActivity(boolean showDefaultActivity, boolean highlightDefaultActivity)842         public void setShowDefaultActivity(boolean showDefaultActivity,
843                 boolean highlightDefaultActivity) {
844             if (mShowDefaultActivity != showDefaultActivity
845                     || mHighlightDefaultActivity != highlightDefaultActivity) {
846                 mShowDefaultActivity = showDefaultActivity;
847                 mHighlightDefaultActivity = highlightDefaultActivity;
848                 notifyDataSetChanged();
849             }
850         }
851 
getShowDefaultActivity()852         public boolean getShowDefaultActivity() {
853             return mShowDefaultActivity;
854         }
855     }
856 
857     /**
858      * Allows us to set the background using TintTypedArray
859      * @hide
860      */
861     @RestrictTo(LIBRARY_GROUP)
862     public static class InnerLayout extends LinearLayout {
863 
864         private static final int[] TINT_ATTRS = {
865                 android.R.attr.background
866         };
867 
InnerLayout(Context context, AttributeSet attrs)868         public InnerLayout(Context context, AttributeSet attrs) {
869             super(context, attrs);
870             TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, TINT_ATTRS);
871             setBackgroundDrawable(a.getDrawable(0));
872             a.recycle();
873         }
874     }
875 }
876