diff options
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); |