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