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
17package android.support.v7.widget;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.pm.PackageManager;
22import android.content.pm.ResolveInfo;
23import android.graphics.drawable.Drawable;
24import android.os.Build;
25import android.support.v4.view.ActionProvider;
26import android.support.v7.appcompat.R;
27import android.support.v7.widget.ActivityChooserModel.OnChooseActivityListener;
28import android.util.TypedValue;
29import android.view.Menu;
30import android.view.MenuItem;
31import android.view.MenuItem.OnMenuItemClickListener;
32import android.view.SubMenu;
33import android.view.View;
34
35/**
36 * Provides a share action, which is suitable for an activity's app bar. Creates
37 * views that enable data sharing. If the provider appears in the
38 * overflow menu, it creates a submenu with the appropriate sharing
39 * actions.
40 *
41 * <h3 id="add-share-action">Adding a share action</h3>
42 *
43 * <p>To add a "share" action to your activity, put a
44 * <code>ShareActionProvider</code> in the app bar's menu resource. For
45 * example:</p>
46 *
47 * <pre>
48 * &lt;item android:id="&#64;+id/action_share"
49 *      android:title="&#64;string/share"
50 *      app:showAsAction="ifRoom"
51 *      app:actionProviderClass="android.support.v7.widget.ShareActionProvider"/&gt;
52 * </pre>
53 *
54 * <p>You do not need to specify an icon, since the
55 * <code>ShareActionProvider</code> widget takes care of its own appearance and
56 * behavior. However, you do need to specify a title with
57 * <code>android:title</code>, in case the action ends up in the overflow
58 * menu.</p>
59 *
60 * <p>Next, set up the intent that contains the content your activity is
61 * able to share. You should create this intent in your handler for
62 * {@link android.app.Activity#onCreateOptionsMenu onCreateOptionsMenu()},
63 * and update it every time the shareable content changes. To set up the
64 * intent:</p>
65 *
66 * <ol>
67 * <li>Get a reference to the ShareActionProvider by calling {@link
68 * android.view.MenuItem#getActionProvider getActionProvider()} and
69 * passing the share action's {@link android.view.MenuItem}. For
70 * example:
71 *
72 * <pre>
73 * MenuItem shareItem = menu.findItem(R.id.action_share);
74 * ShareActionProvider myShareActionProvider =
75 *     (ShareActionProvider) MenuItemCompat.getActionProvider(shareItem);</pre></li>
76 *
77 * <li>Create an intent with the {@link android.content.Intent#ACTION_SEND}
78 * action, and attach the content shared by the activity. For example, the
79 * following intent shares an image:
80 *
81 * <pre>
82 * Intent myShareIntent = new Intent(Intent.ACTION_SEND);
83 * myShareIntent.setType("image/*");
84 * myShareIntent.putExtra(Intent.EXTRA_STREAM, myImageUri);</pre></li>
85 *
86 * <li>Call {@link #setShareIntent setShareIntent()} to attach this intent to
87 * the action provider:
88 *
89 * <pre>
90 * myShareActionProvider.setShareIntent(myShareIntent);
91 * </pre></li>
92 *
93 * <li>When the content changes, modify the intent or create a new one,
94 * and call {@link #setShareIntent setShareIntent()} again. For example:
95 *
96 * <pre>
97 * // Image has changed! Update the intent:
98 * myShareIntent.putExtra(Intent.EXTRA_STREAM, myNewImageUri);
99 * myShareActionProvider.setShareIntent(myShareIntent);</pre></li>
100 * </ol>
101 *
102 * <h3 id="rankings">Share target rankings</h3>
103 *
104 * <p>The share action provider retains a ranking for each share target,
105 * based on how often the user chooses each one. The more often a user
106 * chooses a target, the higher its rank; the
107 * most-commonly used target appears in the app bar as the default target.</p>
108 *
109 * <p>By default, the target ranking information is stored in a private
110 * file with the name specified by {@link
111 * #DEFAULT_SHARE_HISTORY_FILE_NAME}. Ordinarily, the share action provider stores
112 * all the history in this single file. However, using a single set of
113 * rankings may not make sense if the
114 * share action provider is used for different kinds of content. For
115 * example, if the activity sometimes shares images and sometimes shares
116 * contacts, you would want to maintain two different sets of rankings.</p>
117 *
118 * <p>To set the history file, call {@link #setShareHistoryFileName
119 * setShareHistoryFileName()} and pass the name of an XML file. The file
120 * you specify is used until the next time you call {@link
121 * #setShareHistoryFileName setShareHistoryFileName()}.</p>
122 *
123 * @see ActionProvider
124 */
125public class ShareActionProvider extends ActionProvider {
126
127    /**
128     * Listener for the event of selecting a share target.
129     */
130    public interface OnShareTargetSelectedListener {
131
132        /**
133         * Called when a share target has been selected. The client can
134         * decide whether to perform some action before the sharing is
135         * actually performed.
136         * <p>
137         * <strong>Note:</strong> Modifying the intent is not permitted and
138         *     any changes to the latter will be ignored.
139         * </p>
140         * <p>
141         * <strong>Note:</strong> You should <strong>not</strong> handle the
142         *     intent here. This callback aims to notify the client that a
143         *     sharing is being performed, so the client can update the UI
144         *     if necessary.
145         * </p>
146         *
147         * @param source The source of the notification.
148         * @param intent The intent for launching the chosen share target.
149         * @return The return result is ignored. Always return false for consistency.
150         */
151        public boolean onShareTargetSelected(ShareActionProvider source, Intent intent);
152    }
153
154    /**
155     * The default for the maximal number of activities shown in the sub-menu.
156     */
157    private static final int DEFAULT_INITIAL_ACTIVITY_COUNT = 4;
158
159    /**
160     * The the maximum number activities shown in the sub-menu.
161     */
162    private int mMaxShownActivityCount = DEFAULT_INITIAL_ACTIVITY_COUNT;
163
164    /**
165     * Listener for handling menu item clicks.
166     */
167    private final ShareMenuItemOnMenuItemClickListener mOnMenuItemClickListener =
168            new ShareMenuItemOnMenuItemClickListener();
169
170    /**
171     * The default name for storing share history.
172     */
173    public static final String DEFAULT_SHARE_HISTORY_FILE_NAME = "share_history.xml";
174
175    /**
176     * Context for accessing resources.
177     */
178    private final Context mContext;
179
180    /**
181     * The name of the file with share history data.
182     */
183    private String mShareHistoryFileName = DEFAULT_SHARE_HISTORY_FILE_NAME;
184
185    private OnShareTargetSelectedListener mOnShareTargetSelectedListener;
186
187    private OnChooseActivityListener mOnChooseActivityListener;
188
189    /**
190     * Creates a new instance.
191     *
192     * @param context Context for accessing resources.
193     */
194    public ShareActionProvider(Context context) {
195        super(context);
196        mContext = context;
197    }
198
199    /**
200     * Sets a listener to be notified when a share target has been selected.
201     * The listener can optionally decide to handle the selection and
202     * not rely on the default behavior which is to launch the activity.
203     * <p>
204     * <strong>Note:</strong> If you choose the backing share history file
205     *     you will still be notified in this callback.
206     * </p>
207     * @param listener The listener.
208     */
209    public void setOnShareTargetSelectedListener(OnShareTargetSelectedListener listener) {
210        mOnShareTargetSelectedListener = listener;
211        setActivityChooserPolicyIfNeeded();
212    }
213
214    /**
215     * {@inheritDoc}
216     */
217    @Override
218    public View onCreateActionView() {
219        // Create the view and set its data model.
220        ActivityChooserView activityChooserView = new ActivityChooserView(mContext);
221        if (!activityChooserView.isInEditMode()) {
222            ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
223            activityChooserView.setActivityChooserModel(dataModel);
224        }
225
226        // Lookup and set the expand action icon.
227        TypedValue outTypedValue = new TypedValue();
228        mContext.getTheme().resolveAttribute(R.attr.actionModeShareDrawable, outTypedValue, true);
229        Drawable drawable = AppCompatDrawableManager.get()
230                .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
247    public boolean hasSubMenu() {
248        return true;
249    }
250
251    /**
252     * {@inheritDoc}
253     */
254    @Override
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     */
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     */
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
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     */
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
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
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