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

Commit a6f5c70b authored by Philip P. Moltmann's avatar Philip P. Moltmann
Browse files

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
parent 96930e4f
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -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 {
+261 −46
Original line number Diff line number Diff line
@@ -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,67 +218,227 @@ 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();
        }

        /**
     * 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.
         * 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);
        }
    }

    /**
     * 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. &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 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;
            }

            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(' ');

        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();

        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,
            return TextUtils.ellipsize(labelStr.toString(), paint, ellipsizeDip,
                    TextUtils.TruncateAt.END);
        }
    }

    /**
     * Retrieve the current graphical icon associated with this item.  This
+5 −1
Original line number Diff line number Diff line
@@ -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;
+9 −4
Original line number Diff line number Diff line
@@ -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)
+5 −2
Original line number Diff line number Diff line
@@ -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 {
Loading