diff options
author | Philip P. Moltmann <moltmann@google.com> | 2018-04-23 09:09:16 -0700 |
---|---|---|
committer | Philip P. Moltmann <moltmann@google.com> | 2018-05-08 11:27:41 -0700 |
commit | a6f5c70b91ca7dc7c3a331d6ddce37ee64d14fe6 (patch) | |
tree | 0e6ed8859057733787b7092b3fbb57d26f63a0c3 | |
parent | 96930e4faad8ec28e2bbfeffeed8f02109fdcc47 (diff) |
Allow to influence how loadSafeLabel works
Also remove all references to old loadSafeLabel method to prevent
proliferation of old method via copy+paste.
The implementation assumes that there are three cases:
- Short labels that don't have anything wrong with them
- Labels that are fine, but are a little too long
- Intentionally bad label that try to break stuff and slow things down.
In the first two cases no characters are marked for removal, in the
third case we have an implementation that does not use a lot of memory
and has linear performance when there are a lot of bad characters.
Test: gts-tradefed run gts-dev -m GtsContentTestCases
Bug: 77964730
Change-Id: I3feb17b2a12018cd5407c88fe3603f2ebbc9d14e
8 files changed, 300 insertions, 59 deletions
diff --git a/api/system-current.txt b/api/system-current.txt index d62c43ed04c9..ea1f1a145042 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -1062,7 +1062,11 @@ package android.content.pm { } public class PackageItemInfo { - method public java.lang.CharSequence loadSafeLabel(android.content.pm.PackageManager); + method public deprecated java.lang.CharSequence loadSafeLabel(android.content.pm.PackageManager); + method public java.lang.CharSequence loadSafeLabel(android.content.pm.PackageManager, float, int); + field public static final int SAFE_LABEL_FLAG_FIRST_LINE = 4; // 0x4 + field public static final int SAFE_LABEL_FLAG_SINGLE_LINE = 2; // 0x2 + field public static final int SAFE_LABEL_FLAG_TRIM = 1; // 0x1 } public abstract class PackageManager { diff --git a/core/java/android/content/pm/PackageItemInfo.java b/core/java/android/content/pm/PackageItemInfo.java index 07fbfb50f193..14d3f91ffba0 100644 --- a/core/java/android/content/pm/PackageItemInfo.java +++ b/core/java/android/content/pm/PackageItemInfo.java @@ -16,6 +16,10 @@ package android.content.pm; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.FloatRange; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SystemApi; import android.content.res.XmlResourceParser; @@ -29,7 +33,11 @@ import android.text.TextUtils; import android.util.Printer; import android.util.proto.ProtoOutputStream; +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; import java.text.Collator; +import java.util.BitSet; import java.util.Comparator; /** @@ -42,10 +50,56 @@ import java.util.Comparator; * in the implementation of Parcelable in subclasses. */ public class PackageItemInfo { - private static final float MAX_LABEL_SIZE_PX = 500f; + private static final int LINE_FEED_CODE_POINT = 10; + private static final int NBSP_CODE_POINT = 160; + /** The maximum length of a safe label, in characters */ private static final int MAX_SAFE_LABEL_LENGTH = 50000; + /** @hide */ + public static final float DEFAULT_MAX_LABEL_SIZE_PX = 500f; + + /** + * Flags for {@link #loadSafeLabel(PackageManager, float, int)} + * + * @hide + */ + @Retention(SOURCE) + @IntDef(flag = true, prefix = "SAFE_LABEL_FLAG_", + value = {SAFE_LABEL_FLAG_TRIM, SAFE_LABEL_FLAG_SINGLE_LINE, + SAFE_LABEL_FLAG_FIRST_LINE}) + public @interface SafeLabelFlags {} + + /** + * Remove {@link Character#isWhitespace(int) whitespace} and non-breaking spaces from the edges + * of the label. + * + * @see #loadSafeLabel(PackageManager, float, int) + * @hide + */ + @SystemApi + public static final int SAFE_LABEL_FLAG_TRIM = 0x1; + + /** + * Force entire string into single line of text (no newlines). Cannot be set at the same time as + * {@link #SAFE_LABEL_FLAG_FIRST_LINE}. + * + * @see #loadSafeLabel(PackageManager, float, int) + * @hide + */ + @SystemApi + public static final int SAFE_LABEL_FLAG_SINGLE_LINE = 0x2; + + /** + * Return only first line of text (truncate at first newline). Cannot be set at the same time as + * {@link #SAFE_LABEL_FLAG_SINGLE_LINE}. + * + * @see #loadSafeLabel(PackageManager, float, int) + * @hide + */ + @SystemApi + public static final int SAFE_LABEL_FLAG_FIRST_LINE = 0x4; + private static volatile boolean sForceSafeLabels = false; /** {@hide} */ @@ -140,7 +194,8 @@ public class PackageItemInfo { */ public @NonNull CharSequence loadLabel(@NonNull PackageManager pm) { if (sForceSafeLabels) { - return loadSafeLabel(pm); + return loadSafeLabel(pm, DEFAULT_MAX_LABEL_SIZE_PX, SAFE_LABEL_FLAG_TRIM + | SAFE_LABEL_FLAG_FIRST_LINE); } else { return loadUnsafeLabel(pm); } @@ -163,66 +218,226 @@ public class PackageItemInfo { return packageName; } + 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; + } + + /** + * @hide + * @deprecated use loadSafeLabel(PackageManager, float, int) instead + */ + @SystemApi + @Deprecated + public @NonNull CharSequence loadSafeLabel(@NonNull PackageManager pm) { + return loadSafeLabel(pm, DEFAULT_MAX_LABEL_SIZE_PX, SAFE_LABEL_FLAG_TRIM + | SAFE_LABEL_FLAG_FIRST_LINE); + } + + /** + * 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 if a certain {@code offset} of the original string is removed + */ + boolean isRemoved(int offset) { + return mRemovedChars != null && mRemovedChars.get(offset); + } + + /** + * Return codePoint of original string at a certain {@code offset} + */ + int codePointAt(int offset) { + return mOriginal.codePointAt(offset); + } + } + /** - * Same as {@link #loadLabel(PackageManager)} with the addition that - * the returned label is safe for being presented in the UI since it - * will not contain new lines and the length will be limited to a - * reasonable amount. This prevents a malicious party to influence UI - * layout via the app label misleading the user into performing a - * detrimental for them action. If the label is too long it will be - * truncated and ellipsized at the end. + * Load, clean up and truncate label before use. * - * @param pm A PackageManager from which the label can be loaded; usually - * the PackageManager from which you originally retrieved this item - * @return Returns a CharSequence containing the item's label. If the - * item does not have a label, its name is returned. + * <p>This method is meant to remove common mistakes and nefarious formatting from strings that + * are used in sensitive parts of the UI. * + * <p>This method first 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 to a given length + * </ul> + * ... if specified. + * + * @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. + * @return The safe label * @hide */ @SystemApi - public @NonNull CharSequence loadSafeLabel(@NonNull PackageManager pm) { + public @NonNull CharSequence loadSafeLabel(@NonNull PackageManager pm, + @FloatRange(from = 0) float ellipsizeDip, @SafeLabelFlags int flags) { + boolean onlyKeepFirstLine = ((flags & SAFE_LABEL_FLAG_FIRST_LINE) != 0); + boolean forceSingleLine = ((flags & SAFE_LABEL_FLAG_SINGLE_LINE) != 0); + boolean trim = ((flags & SAFE_LABEL_FLAG_TRIM) != 0); + + Preconditions.checkNotNull(pm); + Preconditions.checkArgument(ellipsizeDip >= 0); + Preconditions.checkFlagsArgument(flags, SAFE_LABEL_FLAG_TRIM | SAFE_LABEL_FLAG_SINGLE_LINE + | SAFE_LABEL_FLAG_FIRST_LINE); + Preconditions.checkArgument(!(onlyKeepFirstLine && forceSingleLine), + "Cannot set SAFE_LABEL_FLAG_SINGLE_LINE and SAFE_LABEL_FLAG_FIRST_LINE at the same " + + "time"); + // loadLabel() always returns non-null String label = loadUnsafeLabel(pm).toString(); - // strip HTML tags to avoid <br> and other tags overwriting original message - String labelStr = Html.fromHtml(label).toString(); - - // If the label contains new line characters it may push the UI - // down to hide a part of it. Labels shouldn't have new line - // characters, so just truncate at the first time one is seen. - final int labelLength = Math.min(labelStr.length(), MAX_SAFE_LABEL_LENGTH); - final StringBuffer sb = new StringBuffer(labelLength); - int offset = 0; - while (offset < labelLength) { - final int codePoint = labelStr.codePointAt(offset); - final int type = Character.getType(codePoint); - if (type == Character.LINE_SEPARATOR - || type == Character.CONTROL - || type == Character.PARAGRAPH_SEPARATOR) { - labelStr = labelStr.substring(0, offset); + + // Treat string as HTML. This + // - converts HTML symbols: e.g. ß -> ß + // - 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 labelStr = new StringWithRemovedChars( + Html.fromHtml(label).toString()); + + int firstNonWhiteSpace = -1; + int firstTrailingWhiteSpace = -1; + + // Remove new lines (if requested) and control characters. + int labelLength = labelStr.length(); + for (int offset = 0; offset < labelLength; ) { + int codePoint = labelStr.codePointAt(offset); + int type = Character.getType(codePoint); + int codePointLen = Character.charCount(codePoint); + boolean isNewline = isNewline(codePoint); + + if (offset > MAX_SAFE_LABEL_LENGTH || onlyKeepFirstLine && isNewline) { + labelStr.removeAllCharAfter(offset); break; + } else if (forceSingleLine && isNewline) { + labelStr.removeRange(offset, offset + codePointLen); + } else if (type == Character.CONTROL && !isNewline) { + labelStr.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; } - // replace all non-break space to " " in order to be trimmed - final int charCount = Character.charCount(codePoint); - if (type == Character.SPACE_SEPARATOR) { - sb.append(' '); + + offset += codePointLen; + } + + if (trim) { + // Remove leading and trailing white space + if (firstNonWhiteSpace == -1) { + // No non whitespace found, remove all + labelStr.removeAllCharAfter(0); } else { - sb.append(labelStr.charAt(offset)); - if (charCount == 2) { - sb.append(labelStr.charAt(offset + 1)); + if (firstNonWhiteSpace > 0) { + labelStr.removeAllCharBefore(firstNonWhiteSpace); + } + if (firstTrailingWhiteSpace < labelLength) { + labelStr.removeAllCharAfter(firstTrailingWhiteSpace); } } - offset += charCount; } - labelStr = sb.toString().trim(); - if (labelStr.isEmpty()) { - return packageName; - } - TextPaint paint = new TextPaint(); - paint.setTextSize(42); + if (ellipsizeDip == 0) { + return labelStr.toString(); + } else { + // Truncate + final TextPaint paint = new TextPaint(); + paint.setTextSize(42); - return TextUtils.ellipsize(labelStr, paint, MAX_LABEL_SIZE_PX, - TextUtils.TruncateAt.END); + return TextUtils.ellipsize(labelStr.toString(), paint, ellipsizeDip, + TextUtils.TruncateAt.END); + } } /** diff --git a/core/java/com/android/internal/app/HarmfulAppWarningActivity.java b/core/java/com/android/internal/app/HarmfulAppWarningActivity.java index 99666264cb22..ce2d229d41b3 100644 --- a/core/java/com/android/internal/app/HarmfulAppWarningActivity.java +++ b/core/java/com/android/internal/app/HarmfulAppWarningActivity.java @@ -21,6 +21,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentSender; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.util.Log; @@ -82,7 +83,10 @@ public class HarmfulAppWarningActivity extends AlertActivity implements final View view = getLayoutInflater().inflate(R.layout.harmful_app_warning_dialog, null /*root*/); ((TextView) view.findViewById(R.id.app_name_text)) - .setText(applicationInfo.loadSafeLabel(getPackageManager())); + .setText(applicationInfo.loadSafeLabel(getPackageManager(), + PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, + PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE + | PackageItemInfo.SAFE_LABEL_FLAG_TRIM)); ((TextView) view.findViewById(R.id.message)) .setText(mHarmfulAppWarning); return view; diff --git a/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java b/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java index 56cb88870d65..e6ea2d854356 100644 --- a/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java @@ -21,6 +21,7 @@ import android.app.slice.SliceProvider; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnDismissListener; +import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; @@ -51,10 +52,14 @@ public class SlicePermissionActivity extends Activity implements OnClickListener try { PackageManager pm = getPackageManager(); - CharSequence app1 = BidiFormatter.getInstance().unicodeWrap( - pm.getApplicationInfo(mCallingPkg, 0).loadSafeLabel(pm).toString()); - CharSequence app2 = BidiFormatter.getInstance().unicodeWrap( - pm.getApplicationInfo(mProviderPkg, 0).loadSafeLabel(pm).toString()); + CharSequence app1 = BidiFormatter.getInstance().unicodeWrap(pm.getApplicationInfo( + mCallingPkg, 0).loadSafeLabel(pm, PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, + PackageItemInfo.SAFE_LABEL_FLAG_TRIM + | PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE).toString()); + CharSequence app2 = BidiFormatter.getInstance().unicodeWrap(pm.getApplicationInfo( + mProviderPkg, 0).loadSafeLabel(pm, PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, + PackageItemInfo.SAFE_LABEL_FLAG_TRIM + | PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE).toString()); AlertDialog dialog = new AlertDialog.Builder(this) .setTitle(getString(R.string.slice_permission_title, app1, app2)) .setView(R.layout.slice_permission_request) diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index d44fe4dbc450..3865b2779466 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -23,7 +23,6 @@ import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.internal.util.Preconditions.checkState; import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable; -import android.Manifest; import android.annotation.CheckResult; import android.annotation.Nullable; import android.app.PendingIntent; @@ -39,6 +38,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.FeatureInfo; import android.content.pm.PackageInfo; +import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.net.NetworkPolicyManager; import android.os.Binder; @@ -289,7 +289,10 @@ public class CompanionDeviceManagerService extends SystemService implements Bind String packageTitle = BidiFormatter.getInstance().unicodeWrap( getPackageInfo(callingPackage, userId) .applicationInfo - .loadSafeLabel(getContext().getPackageManager()) + .loadSafeLabel(getContext().getPackageManager(), + PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, + PackageItemInfo.SAFE_LABEL_FLAG_TRIM + | PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE) .toString()); long identity = Binder.clearCallingIdentity(); try { diff --git a/services/core/java/com/android/server/am/DeprecatedTargetSdkVersionDialog.java b/services/core/java/com/android/server/am/DeprecatedTargetSdkVersionDialog.java index d9878cd09fe4..e5add58f287a 100644 --- a/services/core/java/com/android/server/am/DeprecatedTargetSdkVersionDialog.java +++ b/services/core/java/com/android/server/am/DeprecatedTargetSdkVersionDialog.java @@ -23,13 +23,12 @@ import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; -import android.os.Build; import android.os.SystemPropertiesProto; import android.util.Log; import android.view.Window; import android.view.WindowManager; -import android.widget.CheckBox; import com.android.internal.R; import com.android.server.utils.AppInstallerUtil; @@ -45,7 +44,10 @@ public class DeprecatedTargetSdkVersionDialog { mPackageName = appInfo.packageName; final PackageManager pm = context.getPackageManager(); - final CharSequence label = appInfo.loadSafeLabel(pm); + final CharSequence label = appInfo.loadSafeLabel(pm, + PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, + PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE + | PackageItemInfo.SAFE_LABEL_FLAG_TRIM); final CharSequence message = context.getString(R.string.deprecated_target_sdk_message); final AlertDialog.Builder builder = new AlertDialog.Builder(context) diff --git a/services/core/java/com/android/server/am/UnsupportedCompileSdkDialog.java b/services/core/java/com/android/server/am/UnsupportedCompileSdkDialog.java index b6f6ae6b508c..7348a0d5a365 100644 --- a/services/core/java/com/android/server/am/UnsupportedCompileSdkDialog.java +++ b/services/core/java/com/android/server/am/UnsupportedCompileSdkDialog.java @@ -20,6 +20,7 @@ import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.view.Window; import android.view.WindowManager; @@ -37,7 +38,10 @@ public class UnsupportedCompileSdkDialog { mPackageName = appInfo.packageName; final PackageManager pm = context.getPackageManager(); - final CharSequence label = appInfo.loadSafeLabel(pm); + final CharSequence label = appInfo.loadSafeLabel(pm, + PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, + PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE + | PackageItemInfo.SAFE_LABEL_FLAG_TRIM); final CharSequence message = context.getString(R.string.unsupported_compile_sdk_message, label); diff --git a/services/core/java/com/android/server/am/UnsupportedDisplaySizeDialog.java b/services/core/java/com/android/server/am/UnsupportedDisplaySizeDialog.java index 88506632d7c3..1d6438c6e79f 100644 --- a/services/core/java/com/android/server/am/UnsupportedDisplaySizeDialog.java +++ b/services/core/java/com/android/server/am/UnsupportedDisplaySizeDialog.java @@ -21,6 +21,7 @@ import com.android.internal.R; import android.app.AlertDialog; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.view.Window; import android.view.WindowManager; @@ -35,7 +36,10 @@ public class UnsupportedDisplaySizeDialog { mPackageName = appInfo.packageName; final PackageManager pm = context.getPackageManager(); - final CharSequence label = appInfo.loadSafeLabel(pm); + final CharSequence label = appInfo.loadSafeLabel(pm, + PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, + PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE + | PackageItemInfo.SAFE_LABEL_FLAG_TRIM); final CharSequence message = context.getString( R.string.unsupported_display_size_message, label); |