• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.support.v7.widget;
18 
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.pm.PackageManager;
22 import android.content.pm.ResolveInfo;
23 import android.graphics.drawable.Drawable;
24 import android.os.Build;
25 import android.support.v4.view.ActionProvider;
26 import android.support.v7.appcompat.R;
27 import android.support.v7.content.res.AppCompatResources;
28 import android.support.v7.widget.ActivityChooserModel.OnChooseActivityListener;
29 import android.util.TypedValue;
30 import android.view.Menu;
31 import android.view.MenuItem;
32 import android.view.MenuItem.OnMenuItemClickListener;
33 import android.view.SubMenu;
34 import android.view.View;
35 
36 /**
37  * Provides a share action, which is suitable for an activity's app bar. Creates
38  * views that enable data sharing. If the provider appears in the
39  * overflow menu, it creates a submenu with the appropriate sharing
40  * actions.
41  *
42  * <h3 id="add-share-action">Adding a share action</h3>
43  *
44  * <p>To add a "share" action to your activity, put a
45  * <code>ShareActionProvider</code> in the app bar's menu resource. For
46  * example:</p>
47  *
48  * <pre>
49  * &lt;item android:id="&#64;+id/action_share"
50  *      android:title="&#64;string/share"
51  *      app:showAsAction="ifRoom"
52  *      app:actionProviderClass="android.support.v7.widget.ShareActionProvider"/&gt;
53  * </pre>
54  *
55  * <p>You do not need to specify an icon, since the
56  * <code>ShareActionProvider</code> widget takes care of its own appearance and
57  * behavior. However, you do need to specify a title with
58  * <code>android:title</code>, in case the action ends up in the overflow
59  * menu.</p>
60  *
61  * <p>Next, set up the intent that contains the content your activity is
62  * able to share. You should create this intent in your handler for
63  * {@link android.app.Activity#onCreateOptionsMenu onCreateOptionsMenu()},
64  * and update it every time the shareable content changes. To set up the
65  * intent:</p>
66  *
67  * <ol>
68  * <li>Get a reference to the ShareActionProvider by calling {@link
69  * android.view.MenuItem#getActionProvider getActionProvider()} and
70  * passing the share action's {@link android.view.MenuItem}. For
71  * example:
72  *
73  * <pre>
74  * MenuItem shareItem = menu.findItem(R.id.action_share);
75  * ShareActionProvider myShareActionProvider =
76  *     (ShareActionProvider) MenuItemCompat.getActionProvider(shareItem);</pre></li>
77  *
78  * <li>Create an intent with the {@link android.content.Intent#ACTION_SEND}
79  * action, and attach the content shared by the activity. For example, the
80  * following intent shares an image:
81  *
82  * <pre>
83  * Intent myShareIntent = new Intent(Intent.ACTION_SEND);
84  * myShareIntent.setType("image/*");
85  * myShareIntent.putExtra(Intent.EXTRA_STREAM, myImageUri);</pre></li>
86  *
87  * <li>Call {@link #setShareIntent setShareIntent()} to attach this intent to
88  * the action provider:
89  *
90  * <pre>
91  * myShareActionProvider.setShareIntent(myShareIntent);
92  * </pre></li>
93  *
94  * <li>When the content changes, modify the intent or create a new one,
95  * and call {@link #setShareIntent setShareIntent()} again. For example:
96  *
97  * <pre>
98  * // Image has changed! Update the intent:
99  * myShareIntent.putExtra(Intent.EXTRA_STREAM, myNewImageUri);
100  * myShareActionProvider.setShareIntent(myShareIntent);</pre></li>
101  * </ol>
102  *
103  * <h3 id="rankings">Share target rankings</h3>
104  *
105  * <p>The share action provider retains a ranking for each share target,
106  * based on how often the user chooses each one. The more often a user
107  * chooses a target, the higher its rank; the
108  * most-commonly used target appears in the app bar as the default target.</p>
109  *
110  * <p>By default, the target ranking information is stored in a private
111  * file with the name specified by {@link
112  * #DEFAULT_SHARE_HISTORY_FILE_NAME}. Ordinarily, the share action provider stores
113  * all the history in this single file. However, using a single set of
114  * rankings may not make sense if the
115  * share action provider is used for different kinds of content. For
116  * example, if the activity sometimes shares images and sometimes shares
117  * contacts, you would want to maintain two different sets of rankings.</p>
118  *
119  * <p>To set the history file, call {@link #setShareHistoryFileName
120  * setShareHistoryFileName()} and pass the name of an XML file. The file
121  * you specify is used until the next time you call {@link
122  * #setShareHistoryFileName setShareHistoryFileName()}.</p>
123  *
124  * @see ActionProvider
125  */
126 public class ShareActionProvider extends ActionProvider {
127 
128     /**
129      * Listener for the event of selecting a share target.
130      */
131     public interface OnShareTargetSelectedListener {
132 
133         /**
134          * Called when a share target has been selected. The client can
135          * decide whether to perform some action before the sharing is
136          * actually performed.
137          * <p>
138          * <strong>Note:</strong> Modifying the intent is not permitted and
139          *     any changes to the latter will be ignored.
140          * </p>
141          * <p>
142          * <strong>Note:</strong> You should <strong>not</strong> handle the
143          *     intent here. This callback aims to notify the client that a
144          *     sharing is being performed, so the client can update the UI
145          *     if necessary.
146          * </p>
147          *
148          * @param source The source of the notification.
149          * @param intent The intent for launching the chosen share target.
150          * @return The return result is ignored. Always return false for consistency.
151          */
onShareTargetSelected(ShareActionProvider source, Intent intent)152         public boolean onShareTargetSelected(ShareActionProvider source, Intent intent);
153     }
154 
155     /**
156      * The default for the maximal number of activities shown in the sub-menu.
157      */
158     private static final int DEFAULT_INITIAL_ACTIVITY_COUNT = 4;
159 
160     /**
161      * The the maximum number activities shown in the sub-menu.
162      */
163     private int mMaxShownActivityCount = DEFAULT_INITIAL_ACTIVITY_COUNT;
164 
165     /**
166      * Listener for handling menu item clicks.
167      */
168     private final ShareMenuItemOnMenuItemClickListener mOnMenuItemClickListener =
169             new ShareMenuItemOnMenuItemClickListener();
170 
171     /**
172      * The default name for storing share history.
173      */
174     public static final String DEFAULT_SHARE_HISTORY_FILE_NAME = "share_history.xml";
175 
176     /**
177      * Context for accessing resources.
178      */
179     private final Context mContext;
180 
181     /**
182      * The name of the file with share history data.
183      */
184     private String mShareHistoryFileName = DEFAULT_SHARE_HISTORY_FILE_NAME;
185 
186     private OnShareTargetSelectedListener mOnShareTargetSelectedListener;
187 
188     private OnChooseActivityListener mOnChooseActivityListener;
189 
190     /**
191      * Creates a new instance.
192      *
193      * @param context Context for accessing resources.
194      */
ShareActionProvider(Context context)195     public ShareActionProvider(Context context) {
196         super(context);
197         mContext = context;
198     }
199 
200     /**
201      * Sets a listener to be notified when a share target has been selected.
202      * The listener can optionally decide to handle the selection and
203      * not rely on the default behavior which is to launch the activity.
204      * <p>
205      * <strong>Note:</strong> If you choose the backing share history file
206      *     you will still be notified in this callback.
207      * </p>
208      * @param listener The listener.
209      */
setOnShareTargetSelectedListener(OnShareTargetSelectedListener listener)210     public void setOnShareTargetSelectedListener(OnShareTargetSelectedListener listener) {
211         mOnShareTargetSelectedListener = listener;
212         setActivityChooserPolicyIfNeeded();
213     }
214 
215     /**
216      * {@inheritDoc}
217      */
218     @Override
onCreateActionView()219     public View onCreateActionView() {
220         // Create the view and set its data model.
221         ActivityChooserView activityChooserView = new ActivityChooserView(mContext);
222         if (!activityChooserView.isInEditMode()) {
223             ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
224             activityChooserView.setActivityChooserModel(dataModel);
225         }
226 
227         // Lookup and set the expand action icon.
228         TypedValue outTypedValue = new TypedValue();
229         mContext.getTheme().resolveAttribute(R.attr.actionModeShareDrawable, outTypedValue, true);
230         Drawable drawable = AppCompatResources.getDrawable(mContext, outTypedValue.resourceId);
231         activityChooserView.setExpandActivityOverflowButtonDrawable(drawable);
232         activityChooserView.setProvider(this);
233 
234         // Set content description.
235         activityChooserView.setDefaultActionButtonContentDescription(
236                 R.string.abc_shareactionprovider_share_with_application);
237         activityChooserView.setExpandActivityOverflowButtonContentDescription(
238                 R.string.abc_shareactionprovider_share_with);
239 
240         return activityChooserView;
241     }
242 
243     /**
244      * {@inheritDoc}
245      */
246     @Override
hasSubMenu()247     public boolean hasSubMenu() {
248         return true;
249     }
250 
251     /**
252      * {@inheritDoc}
253      */
254     @Override
onPrepareSubMenu(SubMenu subMenu)255     public void onPrepareSubMenu(SubMenu subMenu) {
256         // Clear since the order of items may change.
257         subMenu.clear();
258 
259         ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
260         PackageManager packageManager = mContext.getPackageManager();
261 
262         final int expandedActivityCount = dataModel.getActivityCount();
263         final int collapsedActivityCount = Math.min(expandedActivityCount, mMaxShownActivityCount);
264 
265         // Populate the sub-menu with a sub set of the activities.
266         for (int i = 0; i < collapsedActivityCount; i++) {
267             ResolveInfo activity = dataModel.getActivity(i);
268             subMenu.add(0, i, i, activity.loadLabel(packageManager))
269                     .setIcon(activity.loadIcon(packageManager))
270                     .setOnMenuItemClickListener(mOnMenuItemClickListener);
271         }
272 
273         if (collapsedActivityCount < expandedActivityCount) {
274             // Add a sub-menu for showing all activities as a list item.
275             SubMenu expandedSubMenu = subMenu.addSubMenu(Menu.NONE, collapsedActivityCount,
276                     collapsedActivityCount,
277                     mContext.getString(R.string.abc_activity_chooser_view_see_all));
278             for (int i = 0; i < expandedActivityCount; i++) {
279                 ResolveInfo activity = dataModel.getActivity(i);
280                 expandedSubMenu.add(0, i, i, activity.loadLabel(packageManager))
281                         .setIcon(activity.loadIcon(packageManager))
282                         .setOnMenuItemClickListener(mOnMenuItemClickListener);
283             }
284         }
285     }
286 
287     /**
288      * Sets the file name of a file for persisting the share history which
289      * history will be used for ordering share targets. This file will be used
290      * for all view created by {@link #onCreateActionView()}. Defaults to
291      * {@link #DEFAULT_SHARE_HISTORY_FILE_NAME}. Set to <code>null</code>
292      * if share history should not be persisted between sessions.
293      *
294      * <p class="note">
295      * <strong>Note:</strong> The history file name can be set any time, however
296      * only the action views created by {@link #onCreateActionView()} after setting
297      * the file name will be backed by the provided file. Therefore, if you want to
298      * use different history files for sharing specific types of content, every time
299      * you change the history file with {@link #setShareHistoryFileName(String)} you must
300      * call {@link android.support.v7.app.AppCompatActivity#supportInvalidateOptionsMenu()}
301      * to recreate the action view. You should <strong>not</strong> call
302      * {@link android.support.v7.app.AppCompatActivity#supportInvalidateOptionsMenu()} from
303      * {@link android.support.v7.app.AppCompatActivity#onCreateOptionsMenu(Menu)}.
304      *
305      * <pre>
306      * private void doShare(Intent intent) {
307      *     if (IMAGE.equals(intent.getMimeType())) {
308      *         mShareActionProvider.setHistoryFileName(SHARE_IMAGE_HISTORY_FILE_NAME);
309      *     } else if (TEXT.equals(intent.getMimeType())) {
310      *         mShareActionProvider.setHistoryFileName(SHARE_TEXT_HISTORY_FILE_NAME);
311      *     }
312      *     mShareActionProvider.setIntent(intent);
313      *     supportInvalidateOptionsMenu();
314      * }
315      * </pre>
316      *
317      * @param shareHistoryFile The share history file name.
318      */
setShareHistoryFileName(String shareHistoryFile)319     public void setShareHistoryFileName(String shareHistoryFile) {
320         mShareHistoryFileName = shareHistoryFile;
321         setActivityChooserPolicyIfNeeded();
322     }
323 
324     /**
325      * Sets an intent with information about the share action. Here is a
326      * sample for constructing a share intent:
327      *
328      * <pre>
329      *  Intent shareIntent = new Intent(Intent.ACTION_SEND);
330      *  shareIntent.setType("image/*");
331      *  Uri uri = Uri.fromFile(new File(getFilesDir(), "foo.jpg"));
332      *  shareIntent.putExtra(Intent.EXTRA_STREAM, uri.toString());
333      * </pre>
334      *
335      * @param shareIntent The share intent.
336      *
337      * @see Intent#ACTION_SEND
338      * @see Intent#ACTION_SEND_MULTIPLE
339      */
setShareIntent(Intent shareIntent)340     public void setShareIntent(Intent shareIntent) {
341         if (shareIntent != null) {
342             final String action = shareIntent.getAction();
343             if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
344                 updateIntent(shareIntent);
345             }
346         }
347         ActivityChooserModel dataModel = ActivityChooserModel.get(mContext,
348                 mShareHistoryFileName);
349         dataModel.setIntent(shareIntent);
350     }
351 
352     /**
353      * Reusable listener for handling share item clicks.
354      */
355     private class ShareMenuItemOnMenuItemClickListener implements OnMenuItemClickListener {
356         @Override
onMenuItemClick(MenuItem item)357         public boolean onMenuItemClick(MenuItem item) {
358             ActivityChooserModel dataModel = ActivityChooserModel.get(mContext,
359                     mShareHistoryFileName);
360             final int itemId = item.getItemId();
361             Intent launchIntent = dataModel.chooseActivity(itemId);
362             if (launchIntent != null) {
363                 final String action = launchIntent.getAction();
364                 if (Intent.ACTION_SEND.equals(action) ||
365                         Intent.ACTION_SEND_MULTIPLE.equals(action)) {
366                     updateIntent(launchIntent);
367                 }
368                 mContext.startActivity(launchIntent);
369             }
370             return true;
371         }
372     }
373 
374     /**
375      * Set the activity chooser policy of the model backed by the current
376      * share history file if needed which is if there is a registered callback.
377      */
setActivityChooserPolicyIfNeeded()378     private void setActivityChooserPolicyIfNeeded() {
379         if (mOnShareTargetSelectedListener == null) {
380             return;
381         }
382         if (mOnChooseActivityListener == null) {
383             mOnChooseActivityListener = new ShareActivityChooserModelPolicy();
384         }
385         ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
386         dataModel.setOnChooseActivityListener(mOnChooseActivityListener);
387     }
388 
389     /**
390      * Policy that delegates to the {@link OnShareTargetSelectedListener}, if such.
391      */
392     private class ShareActivityChooserModelPolicy implements OnChooseActivityListener {
393         @Override
onChooseActivity(ActivityChooserModel host, Intent intent)394         public boolean onChooseActivity(ActivityChooserModel host, Intent intent) {
395             if (mOnShareTargetSelectedListener != null) {
396                 mOnShareTargetSelectedListener.onShareTargetSelected(
397                         ShareActionProvider.this, intent);
398             }
399             return false;
400         }
401     }
402 
updateIntent(Intent intent)403     private void updateIntent(Intent intent) {
404         if (Build.VERSION.SDK_INT >= 21) {
405             // If we're on Lollipop, we can open the intent as a document
406             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
407                     Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
408         } else {
409             // Else, we will use the old CLEAR_WHEN_TASK_RESET flag
410             intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
411         }
412     }
413 }
414