Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2a07308e authored by Dianne Hackborn's avatar Dianne Hackborn Committed by Android (Google) Code Review
Browse files

Merge "Add direct support for HTML formatted text in ClipData etc."

parents a94afeb5 acb69bb9
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -4833,6 +4833,7 @@ package android.content {
    method public android.content.ClipDescription getDescription();
    method public android.content.ClipData.Item getItemAt(int);
    method public int getItemCount();
    method public static android.content.ClipData newHtmlText(java.lang.CharSequence, java.lang.CharSequence, java.lang.String);
    method public static android.content.ClipData newIntent(java.lang.CharSequence, android.content.Intent);
    method public static android.content.ClipData newPlainText(java.lang.CharSequence, java.lang.CharSequence);
    method public static android.content.ClipData newRawUri(java.lang.CharSequence, android.net.Uri);
@@ -4843,10 +4844,15 @@ package android.content {
  public static class ClipData.Item {
    ctor public ClipData.Item(java.lang.CharSequence);
    ctor public ClipData.Item(java.lang.CharSequence, java.lang.String);
    ctor public ClipData.Item(android.content.Intent);
    ctor public ClipData.Item(android.net.Uri);
    ctor public ClipData.Item(java.lang.CharSequence, android.content.Intent, android.net.Uri);
    ctor public ClipData.Item(java.lang.CharSequence, java.lang.String, android.content.Intent, android.net.Uri);
    method public java.lang.String coerceToHtmlText(android.content.Context);
    method public java.lang.CharSequence coerceToStyledText(android.content.Context);
    method public java.lang.CharSequence coerceToText(android.content.Context);
    method public java.lang.String getHtmlText();
    method public android.content.Intent getIntent();
    method public java.lang.CharSequence getText();
    method public android.net.Uri getUri();
@@ -4864,6 +4870,7 @@ package android.content {
    method public boolean hasMimeType(java.lang.String);
    method public void writeToParcel(android.os.Parcel, int);
    field public static final android.os.Parcelable.Creator CREATOR;
    field public static final java.lang.String MIMETYPE_TEXT_HTML = "text/html";
    field public static final java.lang.String MIMETYPE_TEXT_INTENT = "text/vnd.android.intent";
    field public static final java.lang.String MIMETYPE_TEXT_PLAIN = "text/plain";
    field public static final java.lang.String MIMETYPE_TEXT_URILIST = "text/uri-list";
@@ -5701,6 +5708,7 @@ package android.content {
    field public static final int EXTRA_DOCK_STATE_UNDOCKED = 0; // 0x0
    field public static final java.lang.String EXTRA_DONT_KILL_APP = "android.intent.extra.DONT_KILL_APP";
    field public static final java.lang.String EXTRA_EMAIL = "android.intent.extra.EMAIL";
    field public static final java.lang.String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT";
    field public static final java.lang.String EXTRA_INITIAL_INTENTS = "android.intent.extra.INITIAL_INTENTS";
    field public static final java.lang.String EXTRA_INSTALLER_PACKAGE_NAME = "android.intent.extra.INSTALLER_PACKAGE_NAME";
    field public static final java.lang.String EXTRA_INTENT = "android.intent.extra.INTENT";
@@ -20406,6 +20414,7 @@ package android.text {
  }
  public class Html {
    method public static java.lang.String escapeHtml(java.lang.CharSequence);
    method public static android.text.Spanned fromHtml(java.lang.String);
    method public static android.text.Spanned fromHtml(java.lang.String, android.text.Html.ImageGetter, android.text.Html.TagHandler);
    method public static java.lang.String toHtml(android.text.Spanned);
+288 −11
Original line number Diff line number Diff line
@@ -21,7 +21,12 @@ import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.util.Log;

import java.io.FileInputStream;
@@ -144,6 +149,8 @@ import java.util.ArrayList;
public class ClipData implements Parcelable {
    static final String[] MIMETYPES_TEXT_PLAIN = new String[] {
        ClipDescription.MIMETYPE_TEXT_PLAIN };
    static final String[] MIMETYPES_TEXT_HTML = new String[] {
        ClipDescription.MIMETYPE_TEXT_HTML };
    static final String[] MIMETYPES_TEXT_URILIST = new String[] {
        ClipDescription.MIMETYPE_TEXT_URILIST };
    static final String[] MIMETYPES_TEXT_INTENT = new String[] {
@@ -176,6 +183,7 @@ public class ClipData implements Parcelable {
     */
    public static class Item {
        final CharSequence mText;
        final String mHtmlText;
        final Intent mIntent;
        final Uri mUri;

@@ -184,6 +192,20 @@ public class ClipData implements Parcelable {
         */
        public Item(CharSequence text) {
            mText = text;
            mHtmlText = null;
            mIntent = null;
            mUri = null;
        }

        /**
         * Create an Item consisting of a single block of (possibly styled) text,
         * with an alternative HTML formatted representation.  You <em>must</em>
         * supply a plain text representation in addition to HTML text; coercion
         * will not be done from HTML formated text into plain text.
         */
        public Item(CharSequence text, String htmlText) {
            mText = text;
            mHtmlText = htmlText;
            mIntent = null;
            mUri = null;
        }
@@ -193,6 +215,7 @@ public class ClipData implements Parcelable {
         */
        public Item(Intent intent) {
            mText = null;
            mHtmlText = null;
            mIntent = intent;
            mUri = null;
        }
@@ -202,16 +225,35 @@ public class ClipData implements Parcelable {
         */
        public Item(Uri uri) {
            mText = null;
            mHtmlText = null;
            mIntent = null;
            mUri = uri;
        }

        /**
         * Create a complex Item, containing multiple representations of
         * text, intent, and/or URI.
         * text, Intent, and/or URI.
         */
        public Item(CharSequence text, Intent intent, Uri uri) {
            mText = text;
            mHtmlText = null;
            mIntent = intent;
            mUri = uri;
        }

        /**
         * Create a complex Item, containing multiple representations of
         * text, HTML text, Intent, and/or URI.  If providing HTML text, you
         * <em>must</em> supply a plain text representation as well; coercion
         * will not be done from HTML formated text into plain text.
         */
        public Item(CharSequence text, String htmlText, Intent intent, Uri uri) {
            if (htmlText != null && text == null) {
                throw new IllegalArgumentException(
                        "Plain text must be supplied if HTML text is supplied");
            }
            mText = text;
            mHtmlText = htmlText;
            mIntent = intent;
            mUri = uri;
        }
@@ -223,6 +265,13 @@ public class ClipData implements Parcelable {
            return mText;
        }

        /**
         * Retrieve the raw HTML text contained in this Item.
         */
        public String getHtmlText() {
            return mHtmlText;
        }

        /**
         * Retrieve the raw Intent contained in this Item.
         */
@@ -261,12 +310,14 @@ public class ClipData implements Parcelable {
//BEGIN_INCLUDE(coerceToText)
        public CharSequence coerceToText(Context context) {
            // If this Item has an explicit textual value, simply return that.
            if (mText != null) {
                return mText;
            CharSequence text = getText();
            if (text != null) {
                return text;
            }

            // If this Item has a URI value, try using that.
            if (mUri != null) {
            Uri uri = getUri();
            if (uri != null) {

                // First see if the URI can be opened as a plain text stream
                // (of any sub-type).  If so, this is the best textual
@@ -275,7 +326,7 @@ public class ClipData implements Parcelable {
                try {
                    // Ask for a stream of the desired type.
                    AssetFileDescriptor descr = context.getContentResolver()
                            .openTypedAssetFileDescriptor(mUri, "text/*", null);
                            .openTypedAssetFileDescriptor(uri, "text/*", null);
                    stream = descr.createInputStream();
                    InputStreamReader reader = new InputStreamReader(stream, "UTF-8");

@@ -308,13 +359,14 @@ public class ClipData implements Parcelable {

                // If we couldn't open the URI as a stream, then the URI itself
                // probably serves fairly well as a textual representation.
                return mUri.toString();
                return uri.toString();
            }

            // Finally, if all we have is an Intent, then we can just turn that
            // into text.  Not the most user-friendly thing, but it's something.
            if (mIntent != null) {
                return mIntent.toUri(Intent.URI_INTENT_SCHEME);
            Intent intent = getIntent();
            if (intent != null) {
                return intent.toUri(Intent.URI_INTENT_SCHEME);
            }

            // Shouldn't get here, but just in case...
@@ -322,6 +374,210 @@ public class ClipData implements Parcelable {
        }
//END_INCLUDE(coerceToText)

        /**
         * Like {@link #coerceToHtmlText(Context)}, but any text that would
         * be returned as HTML formatting will be returned as text with
         * style spans.
         * @param context The caller's Context, from which its ContentResolver
         * and other things can be retrieved.
         * @return Returns the item's textual representation.
         */
        public CharSequence coerceToStyledText(Context context) {
            CharSequence text = getText();
            if (text instanceof Spanned) {
                return text;
            }
            String htmlText = getHtmlText();
            if (htmlText != null) {
                try {
                    CharSequence newText = Html.fromHtml(htmlText);
                    if (newText != null) {
                        return newText;
                    }
                } catch (RuntimeException e) {
                    // If anything bad happens, we'll fall back on the plain text.
                }
            }

            if (text != null) {
                return text;
            }
            return coerceToHtmlOrStyledText(context, true);
        }

        /**
         * Turn this item into HTML text, regardless of the type of data it
         * actually contains.
         *
         * <p>The algorithm for deciding what text to return is:
         * <ul>
         * <li> If {@link #getHtmlText} is non-null, return that.
         * <li> If {@link #getText} is non-null, return that, converting to
         * valid HTML text.  If this text contains style spans,
         * {@link Html#toHtml(Spanned) Html.toHtml(Spanned)} is used to
         * convert them to HTML formatting.
         * <li> If {@link #getUri} is non-null, try to retrieve its data
         * as a text stream from its content provider.  If the provider can
         * supply text/html data, that will be preferred and returned as-is.
         * Otherwise, any text/* data will be returned and escaped to HTML.
         * If it is not a content: URI or the content provider does not supply
         * a text representation, HTML text containing a link to the URI
         * will be returned.
         * <li> If {@link #getIntent} is non-null, convert that to an intent:
         * URI and return as an HTML link.
         * <li> Otherwise, return an empty string.
         * </ul>
         *
         * @param context The caller's Context, from which its ContentResolver
         * and other things can be retrieved.
         * @return Returns the item's representation as HTML text.
         */
        public String coerceToHtmlText(Context context) {
            // If the item has an explicit HTML value, simply return that.
            String htmlText = getHtmlText();
            if (htmlText != null) {
                return htmlText;
            }

            // If this Item has a plain text value, return it as HTML.
            CharSequence text = getText();
            if (text != null) {
                if (text instanceof Spanned) {
                    return Html.toHtml((Spanned)text);
                }
                return Html.escapeHtml(text);
            }

            text = coerceToHtmlOrStyledText(context, false);
            return text != null ? text.toString() : null;
        }

        private CharSequence coerceToHtmlOrStyledText(Context context, boolean styled) {
            // If this Item has a URI value, try using that.
            if (mUri != null) {

                // Check to see what data representations the content
                // provider supports.  We would like HTML text, but if that
                // is not possible we'll live with plan text.
                String[] types = context.getContentResolver().getStreamTypes(mUri, "text/*");
                boolean hasHtml = false;
                boolean hasText = false;
                if (types != null) {
                    for (String type : types) {
                        if ("text/html".equals(type)) {
                            hasHtml = true;
                        } else if (type.startsWith("text/")) {
                            hasText = true;
                        }
                    }
                }

                // If the provider can serve data we can use, open and load it.
                if (hasHtml || hasText) {
                    FileInputStream stream = null;
                    try {
                        // Ask for a stream of the desired type.
                        AssetFileDescriptor descr = context.getContentResolver()
                                .openTypedAssetFileDescriptor(mUri,
                                        hasHtml ? "text/html" : "text/plain", null);
                        stream = descr.createInputStream();
                        InputStreamReader reader = new InputStreamReader(stream, "UTF-8");

                        // Got it...  copy the stream into a local string and return it.
                        StringBuilder builder = new StringBuilder(128);
                        char[] buffer = new char[8192];
                        int len;
                        while ((len=reader.read(buffer)) > 0) {
                            builder.append(buffer, 0, len);
                        }
                        String text = builder.toString();
                        if (hasHtml) {
                            if (styled) {
                                // We loaded HTML formatted text and the caller
                                // want styled text, convert it.
                                try {
                                    CharSequence newText = Html.fromHtml(text);
                                    return newText != null ? newText : text;
                                } catch (RuntimeException e) {
                                    return text;
                                }
                            } else {
                                // We loaded HTML formatted text and that is what
                                // the caller wants, just return it.
                                return text.toString();
                            }
                        }
                        if (styled) {
                            // We loaded plain text and the caller wants styled
                            // text, that is all we have so return it.
                            return text;
                        } else {
                            // We loaded plain text and the caller wants HTML
                            // text, escape it for HTML.
                            return Html.escapeHtml(text);
                        }

                    } catch (FileNotFoundException e) {
                        // Unable to open content URI as text...  not really an
                        // error, just something to ignore.

                    } catch (IOException e) {
                        // Something bad has happened.
                        Log.w("ClippedData", "Failure loading text", e);
                        return Html.escapeHtml(e.toString());

                    } finally {
                        if (stream != null) {
                            try {
                                stream.close();
                            } catch (IOException e) {
                            }
                        }
                    }
                }

                // If we couldn't open the URI as a stream, then we can build
                // some HTML text with the URI itself.
                // probably serves fairly well as a textual representation.
                if (styled) {
                    return uriToStyledText(mUri.toString());
                } else {
                    return uriToHtml(mUri.toString());
                }
            }

            // Finally, if all we have is an Intent, then we can just turn that
            // into text.  Not the most user-friendly thing, but it's something.
            if (mIntent != null) {
                if (styled) {
                    return uriToStyledText(mIntent.toUri(Intent.URI_INTENT_SCHEME));
                } else {
                    return uriToHtml(mIntent.toUri(Intent.URI_INTENT_SCHEME));
                }
            }

            // Shouldn't get here, but just in case...
            return "";
        }

        private String uriToHtml(String uri) {
            StringBuilder builder = new StringBuilder(256);
            builder.append("<a href=\"");
            builder.append(uri);
            builder.append("\">");
            builder.append(Html.escapeHtml(uri));
            builder.append("</a>");
            return builder.toString();
        }

        private CharSequence uriToStyledText(String uri) {
            SpannableStringBuilder builder = new SpannableStringBuilder();
            builder.append(uri);
            builder.setSpan(new URLSpan(uri), 0, builder.length(),
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            return builder;
        }

        @Override
        public String toString() {
            StringBuilder b = new StringBuilder(128);
@@ -335,7 +591,10 @@ public class ClipData implements Parcelable {

        /** @hide */
        public void toShortString(StringBuilder b) {
            if (mText != null) {
            if (mHtmlText != null) {
                b.append("H:");
                b.append(mHtmlText);
            } else if (mText != null) {
                b.append("T:");
                b.append(mText);
            } else if (mUri != null) {
@@ -408,6 +667,22 @@ public class ClipData implements Parcelable {
        return new ClipData(label, MIMETYPES_TEXT_PLAIN, item);
    }

    /**
     * Create a new ClipData holding data of the type
     * {@link ClipDescription#MIMETYPE_TEXT_HTML}.
     *
     * @param label User-visible label for the clip data.
     * @param text The text of clip as plain text, for receivers that don't
     * handle HTML.  This is required.
     * @param htmlText The actual HTML text in the clip.
     * @return Returns a new ClipData containing the specified data.
     */
    static public ClipData newHtmlText(CharSequence label, CharSequence text,
            String htmlText) {
        Item item = new Item(text, htmlText);
        return new ClipData(label, MIMETYPES_TEXT_HTML, item);
    }

    /**
     * Create a new ClipData holding an Intent with MIME type
     * {@link ClipDescription#MIMETYPE_TEXT_INTENT}.
@@ -574,6 +849,7 @@ public class ClipData implements Parcelable {
        for (int i=0; i<N; i++) {
            Item item = mItems.get(i);
            TextUtils.writeToParcel(item.mText, dest, flags);
            dest.writeString(item.mHtmlText);
            if (item.mIntent != null) {
                dest.writeInt(1);
                item.mIntent.writeToParcel(dest, flags);
@@ -600,9 +876,10 @@ public class ClipData implements Parcelable {
        final int N = in.readInt();
        for (int i=0; i<N; i++) {
            CharSequence text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
            String htmlText = in.readString();
            Intent intent = in.readInt() != 0 ? Intent.CREATOR.createFromParcel(in) : null;
            Uri uri = in.readInt() != 0 ? Uri.CREATOR.createFromParcel(in) : null;
            mItems.add(new Item(text, intent, uri));
            mItems.add(new Item(text, htmlText, intent, uri));
        }
    }

+5 −0
Original line number Diff line number Diff line
@@ -40,6 +40,11 @@ public class ClipDescription implements Parcelable {
     */
    public static final String MIMETYPE_TEXT_PLAIN = "text/plain";

    /**
     * The MIME type for a clip holding HTML text.
     */
    public static final String MIMETYPE_TEXT_HTML = "text/html";

    /**
     * The MIME type for a clip holding one or more URIs.  This should be
     * used for URIs that are meaningful to a user (such as an http: URI).
+1 −1
Original line number Diff line number Diff line
@@ -248,7 +248,7 @@ public abstract class ContentResolver {
     * @param mimeTypeFilter The desired MIME type.  This may be a pattern,
     * such as *\/*, to query for all available MIME types that match the
     * pattern.
     * @return Returns an array of MIME type strings for all availablle
     * @return Returns an array of MIME type strings for all available
     * data streams that match the given mimeTypeFilter.  If there are none,
     * null is returned.
     */
+33 −3
Original line number Diff line number Diff line
@@ -954,7 +954,18 @@ public class Intent implements Parcelable, Cloneable {
     * using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it
     * should be the MIME type of the data in EXTRA_STREAM.  Use {@literal *}/*
     * if the MIME type is unknown (this will only allow senders that can
     * handle generic data streams).
     * handle generic data streams).  If using {@link #EXTRA_TEXT}, you can
     * also optionally supply {@link #EXTRA_HTML_TEXT} for clients to retrieve
     * your text with HTML formatting.
     * <p>
     * As of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}, the data
     * being sent can be supplied through {@link #setClipData(ClipData)}.  This
     * allows you to use {@link #FLAG_GRANT_READ_URI_PERMISSION} when sharing
     * content: URIs and other advanced features of {@link ClipData}.  If
     * using this approach, you still must supply the same data through the
     * {@link #EXTRA_TEXT} or {@link #EXTRA_STREAM} fields described below
     * for compatibility with old applications.  If you don't set a ClipData,
     * it will be copied there for you when calling {@link Context#startActivity(Intent)}.
     * <p>
     * Optional standard extras, which may be interpreted by some recipients as
     * appropriate, are: {@link #EXTRA_EMAIL}, {@link #EXTRA_CC},
@@ -967,11 +978,13 @@ public class Intent implements Parcelable, Cloneable {
    /**
     * Activity Action: Deliver multiple data to someone else.
     * <p>
     * Like ACTION_SEND, except the data is multiple.
     * Like {@link #ACTION_SEND}, except the data is multiple.
     * <p>
     * Input: {@link #getType} is the MIME type of the data being sent.
     * get*ArrayListExtra can have either a {@link #EXTRA_TEXT} or {@link
     * #EXTRA_STREAM} field, containing the data to be sent.
     * #EXTRA_STREAM} field, containing the data to be sent.  If using
     * {@link #EXTRA_TEXT}, you can also optionally supply {@link #EXTRA_HTML_TEXT}
     * for clients to retrieve your text with HTML formatting.
     * <p>
     * Multiple types are supported, and receivers should handle mixed types
     * whenever possible. The right way for the receiver to check them is to
@@ -983,6 +996,15 @@ public class Intent implements Parcelable, Cloneable {
     * be image/jpg, but if you are sending image/jpg and image/png, then the
     * intent's type should be image/*.
     * <p>
     * As of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}, the data
     * being sent can be supplied through {@link #setClipData(ClipData)}.  This
     * allows you to use {@link #FLAG_GRANT_READ_URI_PERMISSION} when sharing
     * content: URIs and other advanced features of {@link ClipData}.  If
     * using this approach, you still must supply the same data through the
     * {@link #EXTRA_TEXT} or {@link #EXTRA_STREAM} fields described below
     * for compatibility with old applications.  If you don't set a ClipData,
     * it will be copied there for you when calling {@link Context#startActivity(Intent)}.
     * <p>
     * Optional standard extras, which may be interpreted by some recipients as
     * appropriate, are: {@link #EXTRA_EMAIL}, {@link #EXTRA_CC},
     * {@link #EXTRA_BCC}, {@link #EXTRA_SUBJECT}.
@@ -2500,6 +2522,14 @@ public class Intent implements Parcelable, Cloneable {
     */
    public static final String EXTRA_TEXT = "android.intent.extra.TEXT";

    /**
     * A constant String that is associated with the Intent, used with
     * {@link #ACTION_SEND} to supply an alternative to {@link #EXTRA_TEXT}
     * as HTML formatted text.  Note that you <em>must</em> also supply
     * {@link #EXTRA_TEXT}.
     */
    public static final String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT";

    /**
     * A content: URI holding a stream of data associated with the Intent,
     * used with {@link #ACTION_SEND} to supply the data being sent.
Loading