ShareCompat.java revision 66957a1e997dde850701ee96a3aa7b7c1444d67f
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.support.v4.app;
18
19import android.app.Activity;
20import android.content.ComponentName;
21import android.content.Intent;
22import android.content.pm.PackageManager;
23import android.content.pm.PackageManager.NameNotFoundException;
24import android.graphics.drawable.Drawable;
25import android.net.Uri;
26import android.os.Build;
27import android.support.v4.content.IntentCompat;
28import android.support.v4.view.MenuItemCompat;
29import android.text.Html;
30import android.text.Spanned;
31import android.util.Log;
32import android.view.Menu;
33import android.view.MenuItem;
34
35import java.util.ArrayList;
36
37/**
38 * Extra helper functionality for sharing data between activities.
39 *
40 * ShareCompat provides functionality to extend the {@link Intent#ACTION_SEND}/
41 * {@link Intent#ACTION_SEND_MULTIPLE} protocol and support retrieving more info
42 * about the activity that invoked a social sharing action.
43 *
44 * {@link IntentBuilder} provides helper functions for constructing a sharing
45 * intent that always includes data about the calling activity and app.
46 * This lets the called activity provide attribution for the app that shared
47 * content. Constructing an intent this way can be done in a method-chaining style.
48 * To obtain an IntentBuilder with info about your calling activity, use the static
49 * method {@link IntentBuilder#from(Activity)}.
50 *
51 * {@link IntentReader} provides helper functions for parsing the defined extras
52 * within an {@link Intent#ACTION_SEND} or {@link Intent#ACTION_SEND_MULTIPLE} intent
53 * used to launch an activity. You can also obtain a Drawable for the caller's
54 * application icon and the application's localized label (the app's human-readable name).
55 * Social apps that enable sharing content are encouraged to use this information
56 * to call out the app that the content was shared from.
57 */
58public class ShareCompat {
59    /**
60     * Intent extra that stores the name of the calling package for an ACTION_SEND intent.
61     * When an activity is started using startActivityForResult this is redundant info.
62     * (It is also provided by {@link Activity#getCallingPackage()}.)
63     *
64     * Instead of using this constant directly, consider using {@link #getCallingPackage(Activity)}
65     * or {@link IntentReader#getCallingPackage()}.
66     */
67    public static final String EXTRA_CALLING_PACKAGE =
68            "android.support.v4.app.EXTRA_CALLING_PACKAGE";
69
70    /**
71     * Intent extra that stores the {@link ComponentName} of the calling activity for
72     * an ACTION_SEND intent.
73     */
74    public static final String EXTRA_CALLING_ACTIVITY =
75            "android.support.v4.app.EXTRA_CALLING_ACTIVITY";
76
77    /**
78     * Compatibility shims for sharing operations
79     */
80    interface ShareCompatImpl {
81        void configureMenuItem(MenuItem item, IntentBuilder shareIntent);
82        String escapeHtml(CharSequence text);
83    }
84
85    static class ShareCompatImplBase implements ShareCompatImpl {
86        public void configureMenuItem(MenuItem item, IntentBuilder shareIntent) {
87            item.setIntent(shareIntent.createChooserIntent());
88        }
89
90        public String escapeHtml(CharSequence text) {
91            StringBuilder out = new StringBuilder();
92            withinStyle(out, text, 0, text.length());
93            return out.toString();
94        }
95
96        private static void withinStyle(StringBuilder out, CharSequence text,
97                int start, int end) {
98            for (int i = start; i < end; i++) {
99                char c = text.charAt(i);
100
101                if (c == '<') {
102                    out.append("&lt;");
103                } else if (c == '>') {
104                    out.append("&gt;");
105                } else if (c == '&') {
106                    out.append("&amp;");
107                } else if (c > 0x7E || c < ' ') {
108                    out.append("&#" + ((int) c) + ";");
109                } else if (c == ' ') {
110                    while (i + 1 < end && text.charAt(i + 1) == ' ') {
111                        out.append("&nbsp;");
112                        i++;
113                    }
114
115                    out.append(' ');
116                } else {
117                    out.append(c);
118                }
119            }
120        }
121    }
122
123    static class ShareCompatImplICS extends ShareCompatImplBase {
124        public void configureMenuItem(MenuItem item, IntentBuilder shareIntent) {
125            ShareCompatICS.configureMenuItem(item, shareIntent.getActivity(),
126                    shareIntent.getIntent());
127            if (shouldAddChooserIntent(item)) {
128                item.setIntent(shareIntent.createChooserIntent());
129            }
130        }
131
132        boolean shouldAddChooserIntent(MenuItem item) {
133            return !item.hasSubMenu();
134        }
135    }
136
137    static class ShareCompatImplJB extends ShareCompatImplICS {
138        public String escapeHtml(CharSequence html) {
139            return ShareCompatJB.escapeHtml(html);
140        }
141
142        @Override
143        boolean shouldAddChooserIntent(MenuItem item) {
144            return false;
145        }
146    }
147
148    private static ShareCompatImpl IMPL;
149
150    static {
151        if (Build.VERSION.SDK_INT >= 16) {
152            IMPL = new ShareCompatImplJB();
153        } else if (Build.VERSION.SDK_INT >= 14) {
154            IMPL = new ShareCompatImplICS();
155        } else {
156            IMPL = new ShareCompatImplBase();
157        }
158    }
159
160    /**
161     * Retrieve the name of the package that launched calledActivity from a share intent.
162     * Apps that provide social sharing functionality can use this to provide attribution
163     * for the app that shared the content.
164     *
165     * <p><em>Note:</em> This data may have been provided voluntarily by the calling
166     * application. As such it should not be trusted for accuracy in the context of
167     * security or verification.</p>
168     *
169     * @param calledActivity Current activity that was launched to share content
170     * @return Name of the calling package
171     */
172    public static String getCallingPackage(Activity calledActivity) {
173        String result = calledActivity.getCallingPackage();
174        if (result == null) {
175            result = calledActivity.getIntent().getStringExtra(EXTRA_CALLING_PACKAGE);
176        }
177        return result;
178    }
179
180    /**
181     * Retrieve the ComponentName of the activity that launched calledActivity from a share intent.
182     * Apps that provide social sharing functionality can use this to provide attribution
183     * for the app that shared the content.
184     *
185     * <p><em>Note:</em> This data may have been provided voluntarily by the calling
186     * application. As such it should not be trusted for accuracy in the context of
187     * security or verification.</p>
188     *
189     * @param calledActivity Current activity that was launched to share content
190     * @return ComponentName of the calling activity
191     */
192    public static ComponentName getCallingActivity(Activity calledActivity) {
193        ComponentName result = calledActivity.getCallingActivity();
194        if (result == null) {
195            result = calledActivity.getIntent().getParcelableExtra(EXTRA_CALLING_ACTIVITY);
196        }
197        return result;
198    }
199
200    /**
201     * Configure a {@link MenuItem} to act as a sharing action.
202     *
203     * <p>If the app is running on API level 14 or higher (Android 4.0/Ice Cream Sandwich)
204     * this method will configure a ShareActionProvider to provide a more robust UI
205     * for selecting the target of the share. History will be tracked for each calling
206     * activity in a file named with the prefix ".sharecompat_" in the application's
207     * private data directory. If the application wishes to set this MenuItem to show
208     * as an action in the Action Bar it should use
209     * {@link MenuItemCompat#setShowAsAction(MenuItem, int)} to request that behavior
210     * in addition to calling this method.</p>
211     *
212     * <p>If the app is running on an older platform version this method will configure
213     * a standard activity chooser dialog for the menu item.</p>
214     *
215     * <p>During the calling activity's lifecycle, if data within the share intent must
216     * change the app should change that state in one of several ways:</p>
217     * <ul>
218     * <li>Call {@link ActivityCompat#invalidateOptionsMenu(Activity)}. If the app is running
219     * on API level 11 or above and uses the Action Bar its menu will be recreated and rebuilt.
220     * If not, the activity will receive a call to {@link Activity#onPrepareOptionsMenu(Menu)}
221     * the next time the user presses the menu key to open the options menu panel. The activity
222     * can then call configureMenuItem again with a new or altered IntentBuilder to reconfigure
223     * the share menu item.</li>
224     * <li>Keep a reference to the MenuItem object for the share item once it has been created
225     * and call configureMenuItem to update the associated sharing intent as needed.</li>
226     * </ul>
227     *
228     * @param item MenuItem to configure for sharing
229     * @param shareIntent IntentBuilder with data about the content to share
230     */
231    public static void configureMenuItem(MenuItem item, IntentBuilder shareIntent) {
232        IMPL.configureMenuItem(item, shareIntent);
233    }
234
235    /**
236     * Configure a menu item to act as a sharing action.
237     *
238     * @param menu Menu containing the item to use for sharing
239     * @param menuItemId ID of the share item within menu
240     * @param shareIntent IntentBuilder with data about the content to share
241     * @see #configureMenuItem(MenuItem, IntentBuilder)
242     */
243    public static void configureMenuItem(Menu menu, int menuItemId, IntentBuilder shareIntent) {
244        MenuItem item = menu.findItem(menuItemId);
245        if (item == null) {
246            throw new IllegalArgumentException("Could not find menu item with id " + menuItemId +
247                    " in the supplied menu");
248        }
249        configureMenuItem(item, shareIntent);
250    }
251
252    /**
253     * IntentBuilder is a helper for constructing {@link Intent#ACTION_SEND} and
254     * {@link Intent#ACTION_SEND_MULTIPLE} sharing intents and starting activities
255     * to share content. The ComponentName and package name of the calling activity
256     * will be included.
257     */
258    public static class IntentBuilder {
259        private Activity mActivity;
260        private Intent mIntent;
261        private CharSequence mChooserTitle;
262        private ArrayList<String> mToAddresses;
263        private ArrayList<String> mCcAddresses;
264        private ArrayList<String> mBccAddresses;
265
266        private ArrayList<Uri> mStreams;
267
268        /**
269         * Create a new IntentBuilder for launching a sharing action from launchingActivity.
270         *
271         * @param launchingActivity Activity that the share will be launched from
272         * @return a new IntentBuilder instance
273         */
274        public static IntentBuilder from(Activity launchingActivity) {
275            return new IntentBuilder(launchingActivity);
276        }
277
278        private IntentBuilder(Activity launchingActivity) {
279            mActivity = launchingActivity;
280            mIntent = new Intent().setAction(Intent.ACTION_SEND);
281            mIntent.putExtra(EXTRA_CALLING_PACKAGE, launchingActivity.getPackageName());
282            mIntent.putExtra(EXTRA_CALLING_ACTIVITY, launchingActivity.getComponentName());
283            mIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
284        }
285
286        /**
287         * Retrieve the Intent as configured so far by the IntentBuilder. This Intent
288         * is suitable for use in a ShareActionProvider or chooser dialog.
289         *
290         * <p>To create an intent that will launch the activity chooser so that the user
291         * may select a target for the share, see {@link #createChooserIntent()}.
292         *
293         * @return The current Intent being configured by this builder
294         */
295        public Intent getIntent() {
296            if (mToAddresses != null) {
297                combineArrayExtra(Intent.EXTRA_EMAIL, mToAddresses);
298                mToAddresses = null;
299            }
300            if (mCcAddresses != null) {
301                combineArrayExtra(Intent.EXTRA_CC, mCcAddresses);
302                mCcAddresses = null;
303            }
304            if (mBccAddresses != null) {
305                combineArrayExtra(Intent.EXTRA_BCC, mBccAddresses);
306                mBccAddresses = null;
307            }
308
309            // Check if we need to change the action.
310            boolean needsSendMultiple = mStreams != null && mStreams.size() > 1;
311            boolean isSendMultiple = mIntent.getAction().equals(Intent.ACTION_SEND_MULTIPLE);
312
313            if (!needsSendMultiple && isSendMultiple) {
314                // Change back to a single send action; place the first stream into the
315                // intent for single sharing.
316                mIntent.setAction(Intent.ACTION_SEND);
317                if (mStreams != null && !mStreams.isEmpty()) {
318                    mIntent.putExtra(Intent.EXTRA_STREAM, mStreams.get(0));
319                } else {
320                    mIntent.removeExtra(Intent.EXTRA_STREAM);
321                }
322                mStreams = null;
323            }
324
325            if (needsSendMultiple && !isSendMultiple) {
326                // Change to a multiple send action; place the relevant ArrayList into the
327                // intent for multiple sharing.
328                mIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
329                if (mStreams != null && !mStreams.isEmpty()) {
330                    mIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, mStreams);
331                } else {
332                    mIntent.removeExtra(Intent.EXTRA_STREAM);
333                }
334            }
335
336            return mIntent;
337        }
338
339        Activity getActivity() {
340            return mActivity;
341        }
342
343        private void combineArrayExtra(String extra, ArrayList<String> add) {
344            String[] currentAddresses = mIntent.getStringArrayExtra(extra);
345            int currentLength = currentAddresses != null ? currentAddresses.length : 0;
346            String[] finalAddresses = new String[currentLength + add.size()];
347            add.toArray(finalAddresses);
348            if (currentAddresses != null) {
349                System.arraycopy(currentAddresses, 0, finalAddresses, add.size(), currentLength);
350            }
351            mIntent.putExtra(extra, finalAddresses);
352        }
353
354        private void combineArrayExtra(String extra, String[] add) {
355            // Add any items still pending
356            Intent intent = getIntent();
357            String[] old = intent.getStringArrayExtra(extra);
358            int oldLength = old != null ? old.length : 0;
359            String[] result = new String[oldLength + add.length];
360            if (old != null) System.arraycopy(old, 0, result, 0, oldLength);
361            System.arraycopy(add, 0, result, oldLength, add.length);
362            intent.putExtra(extra, result);
363        }
364
365        /**
366         * Create an Intent that will launch the standard Android activity chooser,
367         * allowing the user to pick what activity/app on the system should handle
368         * the share.
369         *
370         * @return A chooser Intent for the currently configured sharing action
371         */
372        public Intent createChooserIntent() {
373            return Intent.createChooser(getIntent(), mChooserTitle);
374        }
375
376        /**
377         * Start a chooser activity for the current share intent.
378         *
379         * <p>Note that under most circumstances you should use
380         * {@link ShareCompat#configureMenuItem(MenuItem, IntentBuilder)
381         *  ShareCompat.configureMenuItem()} to add a Share item to the menu while
382         * presenting a detail view of the content to be shared instead
383         * of invoking this directly.</p>
384         */
385        public void startChooser() {
386            mActivity.startActivity(createChooserIntent());
387        }
388
389        /**
390         * Set the title that will be used for the activity chooser for this share.
391         *
392         * @param title Title string
393         * @return This IntentBuilder for method chaining
394         */
395        public IntentBuilder setChooserTitle(CharSequence title) {
396            mChooserTitle = title;
397            return this;
398        }
399
400        /**
401         * Set the title that will be used for the activity chooser for this share.
402         *
403         * @param resId Resource ID of the title string to use
404         * @return This IntentBuilder for method chaining
405         */
406        public IntentBuilder setChooserTitle(int resId) {
407            return setChooserTitle(mActivity.getText(resId));
408        }
409
410        /**
411         * Set the type of data being shared
412         *
413         * @param mimeType mimetype of the shared data
414         * @return This IntentBuilder for method chaining
415         * @see Intent#setType(String)
416         */
417        public IntentBuilder setType(String mimeType) {
418            mIntent.setType(mimeType);
419            return this;
420        }
421
422        /**
423         * Set the literal text data to be sent as part of the share.
424         * This may be a styled CharSequence.
425         *
426         * @param text Text to share
427         * @return This IntentBuilder for method chaining
428         * @see Intent#EXTRA_TEXT
429         */
430        public IntentBuilder setText(CharSequence text) {
431            mIntent.putExtra(Intent.EXTRA_TEXT, text);
432            return this;
433        }
434
435        /**
436         * Set an HTML string to be sent as part of the share.
437         * If {@link Intent#EXTRA_TEXT EXTRA_TEXT} has not already been supplied,
438         * a styled version of the supplied HTML text will be added as EXTRA_TEXT as
439         * parsed by {@link android.text.Html#fromHtml(String) Html.fromHtml}.
440         *
441         * @param htmlText A string containing HTML markup as a richer version of the text
442         *                 provided by EXTRA_TEXT.
443         * @return This IntentBuilder for method chaining
444         * @see #setText(CharSequence)
445         */
446        public IntentBuilder setHtmlText(String htmlText) {
447            mIntent.putExtra(IntentCompat.EXTRA_HTML_TEXT, htmlText);
448            if (!mIntent.hasExtra(Intent.EXTRA_TEXT)) {
449                // Supply a default if EXTRA_TEXT isn't set
450                setText(Html.fromHtml(htmlText));
451            }
452            return this;
453        }
454
455        /**
456         * Set a stream URI to the data that should be shared.
457         *
458         * <p>This replaces all currently set stream URIs and will produce a single-stream
459         * ACTION_SEND intent.</p>
460         *
461         * @param streamUri URI of the stream to share
462         * @return This IntentBuilder for method chaining
463         * @see Intent#EXTRA_STREAM
464         */
465        public IntentBuilder setStream(Uri streamUri) {
466            if (!mIntent.getAction().equals(Intent.ACTION_SEND)) {
467                mIntent.setAction(Intent.ACTION_SEND);
468            }
469            mStreams = null;
470            mIntent.putExtra(Intent.EXTRA_STREAM, streamUri);
471            return this;
472        }
473
474        /**
475         * Add a stream URI to the data that should be shared. If this is not the first
476         * stream URI added the final intent constructed will become an ACTION_SEND_MULTIPLE
477         * intent. Not all apps will handle both ACTION_SEND and ACTION_SEND_MULTIPLE.
478         *
479         * @param streamUri URI of the stream to share
480         * @return This IntentBuilder for method chaining
481         * @see Intent#EXTRA_STREAM
482         * @see Intent#ACTION_SEND
483         * @see Intent#ACTION_SEND_MULTIPLE
484         */
485        public IntentBuilder addStream(Uri streamUri) {
486            Uri currentStream = mIntent.getParcelableExtra(Intent.EXTRA_STREAM);
487            if (currentStream == null) {
488                return setStream(streamUri);
489            }
490            if (mStreams == null) {
491                mStreams = new ArrayList<Uri>();
492            }
493            if (currentStream != null) {
494                mIntent.removeExtra(Intent.EXTRA_STREAM);
495                mStreams.add(currentStream);
496            }
497            mStreams.add(streamUri);
498            return this;
499        }
500
501        /**
502         * Set an array of email addresses as recipients of this share.
503         * This replaces all current "to" recipients that have been set so far.
504         *
505         * @param addresses Email addresses to send to
506         * @return This IntentBuilder for method chaining
507         * @see Intent#EXTRA_EMAIL
508         */
509        public IntentBuilder setEmailTo(String[] addresses) {
510            if (mToAddresses != null) {
511                mToAddresses = null;
512            }
513            mIntent.putExtra(Intent.EXTRA_EMAIL, addresses);
514            return this;
515        }
516
517        /**
518         * Add an email address to be used in the "to" field of the final Intent.
519         *
520         * @param address Email address to send to
521         * @return This IntentBuilder for method chaining
522         * @see Intent#EXTRA_EMAIL
523         */
524        public IntentBuilder addEmailTo(String address) {
525            if (mToAddresses == null) {
526                mToAddresses = new ArrayList<String>();
527            }
528            mToAddresses.add(address);
529            return this;
530        }
531
532        /**
533         * Add an array of email addresses to be used in the "to" field of the final Intent.
534         *
535         * @param addresses Email addresses to send to
536         * @return This IntentBuilder for method chaining
537         * @see Intent#EXTRA_EMAIL
538         */
539        public IntentBuilder addEmailTo(String[] addresses) {
540            combineArrayExtra(Intent.EXTRA_EMAIL, addresses);
541            return this;
542        }
543
544        /**
545         * Set an array of email addresses to CC on this share.
546         * This replaces all current "CC" recipients that have been set so far.
547         *
548         * @param addresses Email addresses to CC on the share
549         * @return This IntentBuilder for method chaining
550         * @see Intent#EXTRA_CC
551         */
552        public IntentBuilder setEmailCc(String[] addresses) {
553            mIntent.putExtra(Intent.EXTRA_CC, addresses);
554            return this;
555        }
556
557        /**
558         * Add an email address to be used in the "cc" field of the final Intent.
559         *
560         * @param address Email address to CC
561         * @return This IntentBuilder for method chaining
562         * @see Intent#EXTRA_CC
563         */
564        public IntentBuilder addEmailCc(String address) {
565            if (mCcAddresses == null) {
566                mCcAddresses = new ArrayList<String>();
567            }
568            mCcAddresses.add(address);
569            return this;
570        }
571
572        /**
573         * Add an array of email addresses to be used in the "cc" field of the final Intent.
574         *
575         * @param addresses Email addresses to CC
576         * @return This IntentBuilder for method chaining
577         * @see Intent#EXTRA_CC
578         */
579        public IntentBuilder addEmailCc(String[] addresses) {
580            combineArrayExtra(Intent.EXTRA_CC, addresses);
581            return this;
582        }
583
584        /**
585         * Set an array of email addresses to BCC on this share.
586         * This replaces all current "BCC" recipients that have been set so far.
587         *
588         * @param addresses Email addresses to BCC on the share
589         * @return This IntentBuilder for method chaining
590         * @see Intent#EXTRA_BCC
591         */
592        public IntentBuilder setEmailBcc(String[] addresses) {
593            mIntent.putExtra(Intent.EXTRA_BCC, addresses);
594            return this;
595        }
596
597        /**
598         * Add an email address to be used in the "bcc" field of the final Intent.
599         *
600         * @param address Email address to BCC
601         * @return This IntentBuilder for method chaining
602         * @see Intent#EXTRA_BCC
603         */
604        public IntentBuilder addEmailBcc(String address) {
605            if (mBccAddresses == null) {
606                mBccAddresses = new ArrayList<String>();
607            }
608            mBccAddresses.add(address);
609            return this;
610        }
611
612        /**
613         * Add an array of email addresses to be used in the "bcc" field of the final Intent.
614         *
615         * @param addresses Email addresses to BCC
616         * @return This IntentBuilder for method chaining
617         * @see Intent#EXTRA_BCC
618         */
619        public IntentBuilder addEmailBcc(String[] addresses) {
620            combineArrayExtra(Intent.EXTRA_BCC, addresses);
621            return this;
622        }
623
624        /**
625         * Set a subject heading for this share; useful for sharing via email.
626         *
627         * @param subject Subject heading for this share
628         * @return This IntentBuilder for method chaining
629         * @see Intent#EXTRA_SUBJECT
630         */
631        public IntentBuilder setSubject(String subject) {
632            mIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
633            return this;
634        }
635    }
636
637    /**
638     * IntentReader is a helper for reading the data contained within a sharing (ACTION_SEND)
639     * Intent. It provides methods to parse standard elements included with a share
640     * in addition to extra metadata about the app that shared the content.
641     *
642     * <p>Social sharing apps are encouraged to provide attribution for the app that shared
643     * the content. IntentReader offers access to the application label, calling activity info,
644     * and application icon of the app that shared the content. This data may have been provided
645     * voluntarily by the calling app and should always be displayed to the user before submission
646     * for manual verification. The user should be offered the option to omit this information
647     * from shared posts if desired.</p>
648     *
649     * <p>Activities that intend to receive sharing intents should configure an intent-filter
650     * to accept {@link Intent#ACTION_SEND} intents ("android.intent.action.SEND") and optionally
651     * accept {@link Intent#ACTION_SEND_MULTIPLE} ("android.intent.action.SEND_MULTIPLE") if
652     * the activity is equipped to handle multiple data streams.</p>
653     */
654    public static class IntentReader {
655        private static final String TAG = "IntentReader";
656
657        private Activity mActivity;
658        private Intent mIntent;
659        private String mCallingPackage;
660        private ComponentName mCallingActivity;
661
662        private ArrayList<Uri> mStreams;
663
664        /**
665         * Get an IntentReader for parsing and interpreting the sharing intent
666         * used to start the given activity.
667         *
668         * @param activity Activity that was started to share content
669         * @return IntentReader for parsing sharing data
670         */
671        public static IntentReader from(Activity activity) {
672            return new IntentReader(activity);
673        }
674
675        private IntentReader(Activity activity) {
676            mActivity = activity;
677            mIntent = activity.getIntent();
678            mCallingPackage = ShareCompat.getCallingPackage(activity);
679            mCallingActivity = ShareCompat.getCallingActivity(activity);
680        }
681
682        /**
683         * Returns true if the activity this reader was obtained for was
684         * started with an {@link Intent#ACTION_SEND} or {@link Intent#ACTION_SEND_MULTIPLE}
685         * sharing Intent.
686         *
687         * @return true if the activity was started with an ACTION_SEND
688         *         or ACTION_SEND_MULTIPLE Intent
689         */
690        public boolean isShareIntent() {
691            final String action = mIntent.getAction();
692            return action.equals(Intent.ACTION_SEND) || action.equals(Intent.ACTION_SEND_MULTIPLE);
693        }
694
695        /**
696         * Returns true if the activity this reader was obtained for was started with an
697         * {@link Intent#ACTION_SEND} intent and contains a single shared item.
698         * The shared content should be obtained using either the {@link #getText()}
699         * or {@link #getStream()} methods depending on the type of content shared.
700         *
701         * @return true if the activity was started with an ACTION_SEND intent
702         */
703        public boolean isSingleShare() {
704            return mIntent.getAction().equals(Intent.ACTION_SEND);
705        }
706
707        /**
708         * Returns true if the activity this reader was obtained for was started with an
709         * {@link Intent#ACTION_SEND_MULTIPLE} intent. The Intent may contain more than
710         * one stream item.
711         *
712         * @return true if the activity was started with an ACTION_SEND_MULTIPLE intent
713         */
714        public boolean isMultipleShare() {
715            return mIntent.getAction().equals(Intent.ACTION_SEND_MULTIPLE);
716        }
717
718        /**
719         * Get the mimetype of the data shared to this activity.
720         *
721         * @return mimetype of the shared data
722         * @see Intent#getType()
723         */
724        public String getType() {
725            return mIntent.getType();
726        }
727
728        /**
729         * Get the literal text shared with the target activity.
730         *
731         * @return Literal shared text or null if none was supplied
732         * @see Intent#EXTRA_TEXT
733         */
734        public CharSequence getText() {
735            return mIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
736        }
737
738        /**
739         * Get the styled HTML text shared with the target activity.
740         * If no HTML text was supplied but {@link Intent#EXTRA_TEXT} contained
741         * styled text, it will be converted to HTML if possible and returned.
742         * If the text provided by {@link Intent#EXTRA_TEXT} was not styled text,
743         * it will be escaped by {@link android.text.Html#escapeHtml(CharSequence)}
744         * and returned. If no text was provided at all, this method will return null.
745         *
746         * @return Styled text provided by the sender as HTML.
747         */
748        public String getHtmlText() {
749            String result = mIntent.getStringExtra(IntentCompat.EXTRA_HTML_TEXT);
750            if (mIntent == null) {
751                CharSequence text = getText();
752                if (text instanceof Spanned) {
753                    result = Html.toHtml((Spanned) text);
754                } else if (text != null) {
755                    result = IMPL.escapeHtml(text);
756                }
757            }
758            return result;
759        }
760
761        /**
762         * Get a URI referring to a data stream shared with the target activity.
763         *
764         * <p>This call will fail if the share intent contains multiple stream items.
765         * If {@link #isMultipleShare()} returns true the application should use
766         * {@link #getStream(int)} and {@link #getStreamCount()} to retrieve the
767         * included stream items.</p>
768         *
769         * @return A URI referring to a data stream to be shared or null if one was not supplied
770         * @see Intent#EXTRA_STREAM
771         */
772        public Uri getStream() {
773            return mIntent.getParcelableExtra(Intent.EXTRA_STREAM);
774        }
775
776        /**
777         * Get the URI of a stream item shared with the target activity.
778         * Index should be in the range [0-getStreamCount()).
779         *
780         * @param index Index of text item to retrieve
781         * @return Requested stream item URI
782         * @see Intent#EXTRA_STREAM
783         * @see Intent#ACTION_SEND_MULTIPLE
784         */
785        public Uri getStream(int index) {
786            if (mStreams == null && isMultipleShare()) {
787                mStreams = mIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
788            }
789            if (mStreams != null) {
790                return mStreams.get(index);
791            }
792            if (index == 0) {
793                return mIntent.getParcelableExtra(Intent.EXTRA_STREAM);
794            }
795            throw new IndexOutOfBoundsException("Stream items available: " + getStreamCount() +
796                    " index requested: " + index);
797        }
798
799        /**
800         * Return the number of stream items shared. The return value will be 0 or 1 if
801         * this was an {@link Intent#ACTION_SEND} intent, or 0 or more if it was an
802         * {@link Intent#ACTION_SEND_MULTIPLE} intent.
803         *
804         * @return Count of text items contained within the Intent
805         */
806        public int getStreamCount() {
807            if (mStreams == null && isMultipleShare()) {
808                mStreams = mIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
809            }
810            if (mStreams != null) {
811                return mStreams.size();
812            }
813            return mIntent.hasExtra(Intent.EXTRA_STREAM) ? 1 : 0;
814        }
815
816        /**
817         * Get an array of Strings, each an email address to share to.
818         *
819         * @return An array of email addresses or null if none were supplied.
820         * @see Intent#EXTRA_EMAIL
821         */
822        public String[] getEmailTo() {
823            return mIntent.getStringArrayExtra(Intent.EXTRA_EMAIL);
824        }
825
826        /**
827         * Get an array of Strings, each an email address to CC on this share.
828         *
829         * @return An array of email addresses or null if none were supplied.
830         * @see Intent#EXTRA_CC
831         */
832        public String[] getEmailCc() {
833            return mIntent.getStringArrayExtra(Intent.EXTRA_CC);
834        }
835
836        /**
837         * Get an array of Strings, each an email address to BCC on this share.
838         *
839         * @return An array of email addresses or null if none were supplied.
840         * @see Intent#EXTRA_BCC
841         */
842        public String[] getEmailBcc() {
843            return mIntent.getStringArrayExtra(Intent.EXTRA_BCC);
844        }
845
846        /**
847         * Get a subject heading for this share; useful when sharing via email.
848         *
849         * @return The subject heading for this share or null if one was not supplied.
850         * @see Intent#EXTRA_SUBJECT
851         */
852        public String getSubject() {
853            return mIntent.getStringExtra(Intent.EXTRA_SUBJECT);
854        }
855
856        /**
857         * Get the name of the package that invoked this sharing intent. If the activity
858         * was not started for a result, IntentBuilder will read this from extra metadata placed
859         * in the Intent by ShareBuilder.
860         *
861         * <p><em>Note:</em> This data may have been provided voluntarily by the calling
862         * application. As such it should not be trusted for accuracy in the context of
863         * security or verification.</p>
864         *
865         * @return Name of the package that started this activity or null if unknown
866         * @see Activity#getCallingPackage()
867         * @see ShareCompat#EXTRA_CALLING_PACKAGE
868         */
869        public String getCallingPackage() {
870            return mCallingPackage;
871        }
872
873        /**
874         * Get the {@link ComponentName} of the Activity that invoked this sharing intent.
875         * If the target sharing activity was not started for a result, IntentBuilder will read
876         * this from extra metadata placed in the intent by ShareBuilder.
877         *
878         * <p><em>Note:</em> This data may have been provided voluntarily by the calling
879         * application. As such it should not be trusted for accuracy in the context of
880         * security or verification.</p>
881         *
882         * @return ComponentName of the calling Activity or null if unknown
883         * @see Activity#getCallingActivity()
884         * @see ShareCompat#EXTRA_CALLING_ACTIVITY
885         */
886        public ComponentName getCallingActivity() {
887            return mCallingActivity;
888        }
889
890        /**
891         * Get the icon of the calling activity as a Drawable if data about
892         * the calling activity is available.
893         *
894         * <p><em>Note:</em> This data may have been provided voluntarily by the calling
895         * application. As such it should not be trusted for accuracy in the context of
896         * security or verification.</p>
897         *
898         * @return The calling Activity's icon or null if unknown
899         */
900        public Drawable getCallingActivityIcon() {
901            if (mCallingActivity == null) return null;
902
903            PackageManager pm = mActivity.getPackageManager();
904            try {
905                return pm.getActivityIcon(mCallingActivity);
906            } catch (NameNotFoundException e) {
907                Log.e(TAG, "Could not retrieve icon for calling activity", e);
908            }
909            return null;
910        }
911
912        /**
913         * Get the icon of the calling application as a Drawable if data
914         * about the calling package is available.
915         *
916         * <p><em>Note:</em> This data may have been provided voluntarily by the calling
917         * application. As such it should not be trusted for accuracy in the context of
918         * security or verification.</p>
919         *
920         * @return The calling application's icon or null if unknown
921         */
922        public Drawable getCallingApplicationIcon() {
923            if (mCallingPackage == null) return null;
924
925            PackageManager pm = mActivity.getPackageManager();
926            try {
927                return pm.getApplicationIcon(mCallingPackage);
928            } catch (NameNotFoundException e) {
929                Log.e(TAG, "Could not retrieve icon for calling application", e);
930            }
931            return null;
932        }
933
934        /**
935         * Get the human-readable label (title) of the calling application if
936         * data about the calling package is available.
937         *
938         * <p><em>Note:</em> This data may have been provided voluntarily by the calling
939         * application. As such it should not be trusted for accuracy in the context of
940         * security or verification.</p>
941         *
942         * @return The calling application's label or null if unknown
943         */
944        public CharSequence getCallingApplicationLabel() {
945            if (mCallingPackage == null) return null;
946
947            PackageManager pm = mActivity.getPackageManager();
948            try {
949                return pm.getApplicationLabel(pm.getApplicationInfo(mCallingPackage, 0));
950            } catch (NameNotFoundException e) {
951                Log.e(TAG, "Could not retrieve label for calling application", e);
952            }
953            return null;
954        }
955    }
956}
957