summaryrefslogtreecommitdiff
path: root/core/java/android/text/TextUtils.java
diff options
context:
space:
mode:
authorTreeHugger Robot <treehugger-gerrit@google.com>2018-10-16 16:21:10 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2018-10-16 16:21:10 +0000
commite9944aa1dc1fb0e176f6c4b5719d09055f11152c (patch)
tree3b3f85c53a7b60b4094b431249a81e8bd5c6dfbf /core/java/android/text/TextUtils.java
parent91d56c6b3ce8b1c2669570c46f4d0e06db23036c (diff)
parentc1fda744f06bc775ed51e5e38745a648adf9ecc4 (diff)
Merge "Make loadSafeLabel a generic facility as makeSafeForPresentation"
Diffstat (limited to 'core/java/android/text/TextUtils.java')
-rw-r--r--core/java/android/text/TextUtils.java259
1 files changed, 259 insertions, 0 deletions
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 439c2200b7cf..195de07d8595 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -16,7 +16,10 @@
package android.text;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.annotation.FloatRange;
+import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -70,7 +73,9 @@ import com.android.internal.R;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
import java.lang.reflect.Array;
+import java.util.BitSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
@@ -88,6 +93,44 @@ public class TextUtils {
private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…)
private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER (‥)
+ private static final int LINE_FEED_CODE_POINT = 10;
+ private static final int NBSP_CODE_POINT = 160;
+
+ /**
+ * Flags for {@link #makeSafeForPresentation(String, int, float, int)}
+ *
+ * @hide
+ */
+ @Retention(SOURCE)
+ @IntDef(flag = true, prefix = "CLEAN_STRING_FLAG_",
+ value = {SAFE_STRING_FLAG_TRIM, SAFE_STRING_FLAG_SINGLE_LINE,
+ SAFE_STRING_FLAG_FIRST_LINE})
+ public @interface SafeStringFlags {}
+
+ /**
+ * Remove {@link Character#isWhitespace(int) whitespace} and non-breaking spaces from the edges
+ * of the label.
+ *
+ * @see #makeSafeForPresentation(String, int, float, int)
+ */
+ public static final int SAFE_STRING_FLAG_TRIM = 0x1;
+
+ /**
+ * Force entire string into single line of text (no newlines). Cannot be set at the same time as
+ * {@link #SAFE_STRING_FLAG_FIRST_LINE}.
+ *
+ * @see #makeSafeForPresentation(String, int, float, int)
+ */
+ public static final int SAFE_STRING_FLAG_SINGLE_LINE = 0x2;
+
+ /**
+ * Return only first line of text (truncate at first newline). Cannot be set at the same time as
+ * {@link #SAFE_STRING_FLAG_SINGLE_LINE}.
+ *
+ * @see #makeSafeForPresentation(String, int, float, int)
+ */
+ public static final int SAFE_STRING_FLAG_FIRST_LINE = 0x4;
+
/** {@hide} */
@NonNull
public static String getEllipsisString(@NonNull TextUtils.TruncateAt method) {
@@ -2123,6 +2166,222 @@ public class TextUtils {
return trimmed;
}
+ private static boolean isNewline(int codePoint) {
+ int type = Character.getType(codePoint);
+ return type == Character.PARAGRAPH_SEPARATOR || type == Character.LINE_SEPARATOR
+ || codePoint == LINE_FEED_CODE_POINT;
+ }
+
+ private static boolean isWhiteSpace(int codePoint) {
+ return Character.isWhitespace(codePoint) || codePoint == NBSP_CODE_POINT;
+ }
+
+ /**
+ * Remove html, remove bad characters, and truncate string.
+ *
+ * <p>This method is meant to remove common mistakes and nefarious formatting from strings that
+ * were loaded from untrusted sources (such as other packages).
+ *
+ * <p>This method first {@link Html#fromHtml treats the string like HTML} and then ...
+ * <ul>
+ * <li>Removes new lines or truncates at first new line
+ * <li>Trims the white-space off the end
+ * <li>Truncates the string
+ * </ul>
+ * ... if specified.
+ *
+ * @param unclean The input string
+ * @param maxCharactersToConsider The maximum number of characters of {@code unclean} to
+ * consider from the input string. {@code 0} disables this
+ * feature.
+ * @param ellipsizeDip Assuming maximum length of the string (in dip), assuming font size 42.
+ * This is roughly 50 characters for {@code ellipsizeDip == 1000}.<br />
+ * Usually ellipsizing should be left to the view showing the string. If a
+ * string is used as an input to another string, it might be useful to
+ * control the length of the input string though. {@code 0} disables this
+ * feature.
+ * @param flags Flags controlling cleaning behavior (Can be {@link #SAFE_STRING_FLAG_TRIM},
+ * {@link #SAFE_STRING_FLAG_SINGLE_LINE},
+ * and {@link #SAFE_STRING_FLAG_FIRST_LINE})
+ *
+ * @return The cleaned string
+ */
+ public static @NonNull CharSequence makeSafeForPresentation(@NonNull String unclean,
+ @IntRange(from = 0) int maxCharactersToConsider,
+ @FloatRange(from = 0) float ellipsizeDip, @SafeStringFlags int flags) {
+ boolean onlyKeepFirstLine = ((flags & SAFE_STRING_FLAG_FIRST_LINE) != 0);
+ boolean forceSingleLine = ((flags & SAFE_STRING_FLAG_SINGLE_LINE) != 0);
+ boolean trim = ((flags & SAFE_STRING_FLAG_TRIM) != 0);
+
+ Preconditions.checkNotNull(unclean);
+ Preconditions.checkArgumentNonnegative(maxCharactersToConsider);
+ Preconditions.checkArgumentNonNegative(ellipsizeDip, "ellipsizeDip");
+ Preconditions.checkFlagsArgument(flags, SAFE_STRING_FLAG_TRIM
+ | SAFE_STRING_FLAG_SINGLE_LINE | SAFE_STRING_FLAG_FIRST_LINE);
+ Preconditions.checkArgument(!(onlyKeepFirstLine && forceSingleLine),
+ "Cannot set SAFE_STRING_FLAG_SINGLE_LINE and SAFE_STRING_FLAG_FIRST_LINE at the"
+ + "same time");
+
+ String shortString;
+ if (maxCharactersToConsider > 0) {
+ shortString = unclean.substring(0, Math.min(unclean.length(), maxCharactersToConsider));
+ } else {
+ shortString = unclean;
+ }
+
+ // Treat string as HTML. This
+ // - converts HTML symbols: e.g. &szlig; -> ß
+ // - applies some HTML tags: e.g. <br> -> \n
+ // - removes invalid characters such as \b
+ // - removes html styling, such as <b>
+ // - applies html formatting: e.g. a<p>b</p>c -> a\n\nb\n\nc
+ // - replaces some html tags by "object replacement" markers: <img> -> \ufffc
+ // - Removes leading white space
+ // - Removes all trailing white space beside a single space
+ // - Collapses double white space
+ StringWithRemovedChars gettingCleaned = new StringWithRemovedChars(
+ Html.fromHtml(shortString).toString());
+
+ int firstNonWhiteSpace = -1;
+ int firstTrailingWhiteSpace = -1;
+
+ // Remove new lines (if requested) and control characters.
+ int uncleanLength = gettingCleaned.length();
+ for (int offset = 0; offset < uncleanLength; ) {
+ int codePoint = gettingCleaned.codePointAt(offset);
+ int type = Character.getType(codePoint);
+ int codePointLen = Character.charCount(codePoint);
+ boolean isNewline = isNewline(codePoint);
+
+ if (onlyKeepFirstLine && isNewline) {
+ gettingCleaned.removeAllCharAfter(offset);
+ break;
+ } else if (forceSingleLine && isNewline) {
+ gettingCleaned.removeRange(offset, offset + codePointLen);
+ } else if (type == Character.CONTROL && !isNewline) {
+ gettingCleaned.removeRange(offset, offset + codePointLen);
+ } else if (trim && !isWhiteSpace(codePoint)) {
+ // This is only executed if the code point is not removed
+ if (firstNonWhiteSpace == -1) {
+ firstNonWhiteSpace = offset;
+ }
+ firstTrailingWhiteSpace = offset + codePointLen;
+ }
+
+ offset += codePointLen;
+ }
+
+ if (trim) {
+ // Remove leading and trailing white space
+ if (firstNonWhiteSpace == -1) {
+ // No non whitespace found, remove all
+ gettingCleaned.removeAllCharAfter(0);
+ } else {
+ if (firstNonWhiteSpace > 0) {
+ gettingCleaned.removeAllCharBefore(firstNonWhiteSpace);
+ }
+ if (firstTrailingWhiteSpace < uncleanLength) {
+ gettingCleaned.removeAllCharAfter(firstTrailingWhiteSpace);
+ }
+ }
+ }
+
+ if (ellipsizeDip == 0) {
+ return gettingCleaned.toString();
+ } else {
+ // Truncate
+ final TextPaint paint = new TextPaint();
+ paint.setTextSize(42);
+
+ return TextUtils.ellipsize(gettingCleaned.toString(), paint, ellipsizeDip,
+ TextUtils.TruncateAt.END);
+ }
+ }
+
+ /**
+ * A special string manipulation class. Just records removals and executes the when onString()
+ * is called.
+ */
+ private static class StringWithRemovedChars {
+ /** The original string */
+ private final String mOriginal;
+
+ /**
+ * One bit per char in string. If bit is set, character needs to be removed. If whole
+ * bit field is not initialized nothing needs to be removed.
+ */
+ private BitSet mRemovedChars;
+
+ StringWithRemovedChars(@NonNull String original) {
+ mOriginal = original;
+ }
+
+ /**
+ * Mark all chars in a range {@code [firstRemoved - firstNonRemoved[} (not including
+ * firstNonRemoved) as removed.
+ */
+ void removeRange(int firstRemoved, int firstNonRemoved) {
+ if (mRemovedChars == null) {
+ mRemovedChars = new BitSet(mOriginal.length());
+ }
+
+ mRemovedChars.set(firstRemoved, firstNonRemoved);
+ }
+
+ /**
+ * Remove all characters before {@code firstNonRemoved}.
+ */
+ void removeAllCharBefore(int firstNonRemoved) {
+ if (mRemovedChars == null) {
+ mRemovedChars = new BitSet(mOriginal.length());
+ }
+
+ mRemovedChars.set(0, firstNonRemoved);
+ }
+
+ /**
+ * Remove all characters after and including {@code firstRemoved}.
+ */
+ void removeAllCharAfter(int firstRemoved) {
+ if (mRemovedChars == null) {
+ mRemovedChars = new BitSet(mOriginal.length());
+ }
+
+ mRemovedChars.set(firstRemoved, mOriginal.length());
+ }
+
+ @Override
+ public String toString() {
+ // Common case, no chars removed
+ if (mRemovedChars == null) {
+ return mOriginal;
+ }
+
+ StringBuilder sb = new StringBuilder(mOriginal.length());
+ for (int i = 0; i < mOriginal.length(); i++) {
+ if (!mRemovedChars.get(i)) {
+ sb.append(mOriginal.charAt(i));
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Return length or the original string
+ */
+ int length() {
+ return mOriginal.length();
+ }
+
+ /**
+ * Return codePoint of original string at a certain {@code offset}
+ */
+ int codePointAt(int offset) {
+ return mOriginal.codePointAt(offset);
+ }
+ }
+
private static Object sLock = new Object();
private static char[] sTemp = null;