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