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

Commit ba196c5a authored by Abodunrinwa Toki's avatar Abodunrinwa Toki
Browse files

Do not parcel legacy TextClassification fields

If we depend on legacyIntent, then TextClassifierService implementations
will have to always popuplate a deprecated field.
To avoid breaking legacy clients, the returned legacyOnClickListener should
represent the first pendingIntent (i.e. primary action) that was parcelled.

Bug: 78340399
Test: atest CtsViewTestCases:TextClassificationManagerTest
Test: atest FrameworksCoreTests:TextClassificationTest
Test: manual check with a TCS that only sets non-deprecated fields vs a
legacy TC client
Change-Id: I41d27a65f1ede6369dd2a66d92b2210edb0d11e2
parent 1b5e2d8b
Loading
Loading
Loading
Loading
+61 −66
Original line number Original line Diff line number Diff line
@@ -28,10 +28,11 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.LocaleList;
import android.os.LocaleList;
import android.os.Parcel;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.Parcelable;
@@ -68,7 +69,7 @@ import java.util.Map;
 *   Button button = new Button(context);
 *   Button button = new Button(context);
 *   button.setCompoundDrawablesWithIntrinsicBounds(classification.getIcon(), null, null, null);
 *   button.setCompoundDrawablesWithIntrinsicBounds(classification.getIcon(), null, null, null);
 *   button.setText(classification.getLabel());
 *   button.setText(classification.getLabel());
 *   button.setOnClickListener(v -> context.startActivity(classification.getIntent()));
 *   button.setOnClickListener(v -> classification.getActions().get(0).getActionIntent().send());
 * }</pre>
 * }</pre>
 *
 *
 * <p>e.g. starting an action mode with menu items that can handle the classified text:
 * <p>e.g. starting an action mode with menu items that can handle the classified text:
@@ -194,6 +195,9 @@ public final class TextClassification implements Parcelable {
    /**
    /**
     * Returns an icon that may be rendered on a widget used to act on the classified text.
     * Returns an icon that may be rendered on a widget used to act on the classified text.
     *
     *
     * <p><strong>NOTE: </strong>This field is not parcelable and only represents the icon of the
     * first {@link RemoteAction} (if one exists) when this object is read from a parcel.
     *
     * @deprecated Use {@link #getActions()} instead.
     * @deprecated Use {@link #getActions()} instead.
     */
     */
    @Deprecated
    @Deprecated
@@ -205,6 +209,9 @@ public final class TextClassification implements Parcelable {
    /**
    /**
     * Returns a label that may be rendered on a widget used to act on the classified text.
     * Returns a label that may be rendered on a widget used to act on the classified text.
     *
     *
     * <p><strong>NOTE: </strong>This field is not parcelable and only represents the label of the
     * first {@link RemoteAction} (if one exists) when this object is read from a parcel.
     *
     * @deprecated Use {@link #getActions()} instead.
     * @deprecated Use {@link #getActions()} instead.
     */
     */
    @Deprecated
    @Deprecated
@@ -216,6 +223,9 @@ public final class TextClassification implements Parcelable {
    /**
    /**
     * Returns an intent that may be fired to act on the classified text.
     * Returns an intent that may be fired to act on the classified text.
     *
     *
     * <p><strong>NOTE: </strong>This field is not parcelled and will always return null when this
     * object is read from a parcel.
     *
     * @deprecated Use {@link #getActions()} instead.
     * @deprecated Use {@link #getActions()} instead.
     */
     */
    @Deprecated
    @Deprecated
@@ -225,10 +235,10 @@ public final class TextClassification implements Parcelable {
    }
    }


    /**
    /**
     * Returns the OnClickListener that may be triggered to act on the classified text. This field
     * Returns the OnClickListener that may be triggered to act on the classified text.
     * is not parcelable and will be null for all objects read from a parcel. Instead, call
     *
     * Context#startActivity(Intent) with the result of #getSecondaryIntent(int). Note that this may
     * <p><strong>NOTE: </strong>This field is not parcelable and only represents the first
     * fail if the activity doesn't have permission to send the intent.
     * {@link RemoteAction} (if one exists) when this object is read from a parcel.
     *
     *
     * @deprecated Use {@link #getActions()} instead.
     * @deprecated Use {@link #getActions()} instead.
     */
     */
@@ -323,41 +333,6 @@ public final class TextClassification implements Parcelable {
                || context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
                || context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
    }
    }


    /**
     * Returns a Bitmap representation of the Drawable
     *
     * @param drawable The drawable to convert.
     * @param maxDims The maximum edge length of the resulting bitmap (in pixels).
     */
    @Nullable
    private static Bitmap drawableToBitmap(@Nullable Drawable drawable, int maxDims) {
        if (drawable == null) {
            return null;
        }
        final int actualWidth = Math.max(1, drawable.getIntrinsicWidth());
        final int actualHeight = Math.max(1, drawable.getIntrinsicHeight());
        final double scaleWidth = ((double) maxDims) / actualWidth;
        final double scaleHeight = ((double) maxDims) / actualHeight;
        final double scale = Math.min(1.0, Math.min(scaleWidth, scaleHeight));
        final int width = (int) (actualWidth * scale);
        final int height = (int) (actualHeight * scale);
        if (drawable instanceof BitmapDrawable) {
            final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            if (actualWidth != width || actualHeight != height) {
                return Bitmap.createScaledBitmap(
                        bitmapDrawable.getBitmap(), width, height, /*filter=*/false);
            } else {
                return bitmapDrawable.getBitmap();
            }
        } else {
            final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            final Canvas canvas = new Canvas(bitmap);
            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);
            return bitmap;
        }
    }

    /**
    /**
     * Builder for building {@link TextClassification} objects.
     * Builder for building {@link TextClassification} objects.
     *
     *
@@ -426,6 +401,9 @@ public final class TextClassification implements Parcelable {
         * Sets the icon for the <i>primary</i> action that may be rendered on a widget used to act
         * Sets the icon for the <i>primary</i> action that may be rendered on a widget used to act
         * on the classified text.
         * on the classified text.
         *
         *
         * <p><strong>NOTE: </strong>This field is not parcelled. If read from a parcel, the
         * returned icon represents the icon of the first {@link RemoteAction} (if one exists).
         *
         * @deprecated Use {@link #addAction(RemoteAction)} instead.
         * @deprecated Use {@link #addAction(RemoteAction)} instead.
         */
         */
        @Deprecated
        @Deprecated
@@ -439,6 +417,9 @@ public final class TextClassification implements Parcelable {
         * Sets the label for the <i>primary</i> action that may be rendered on a widget used to
         * Sets the label for the <i>primary</i> action that may be rendered on a widget used to
         * act on the classified text.
         * act on the classified text.
         *
         *
         * <p><strong>NOTE: </strong>This field is not parcelled. If read from a parcel, the
         * returned label represents the label of the first {@link RemoteAction} (if one exists).
         *
         * @deprecated Use {@link #addAction(RemoteAction)} instead.
         * @deprecated Use {@link #addAction(RemoteAction)} instead.
         */
         */
        @Deprecated
        @Deprecated
@@ -452,6 +433,8 @@ public final class TextClassification implements Parcelable {
         * Sets the intent for the <i>primary</i> action that may be fired to act on the classified
         * Sets the intent for the <i>primary</i> action that may be fired to act on the classified
         * text.
         * text.
         *
         *
         * <p><strong>NOTE: </strong>This field is not parcelled.
         *
         * @deprecated Use {@link #addAction(RemoteAction)} instead.
         * @deprecated Use {@link #addAction(RemoteAction)} instead.
         */
         */
        @Deprecated
        @Deprecated
@@ -463,8 +446,10 @@ public final class TextClassification implements Parcelable {


        /**
        /**
         * Sets the OnClickListener for the <i>primary</i> action that may be triggered to act on
         * Sets the OnClickListener for the <i>primary</i> action that may be triggered to act on
         * the classified text. This field is not parcelable and will always be null when the
         * the classified text.
         * object is read from a parcel.
         *
         * <p><strong>NOTE: </strong>This field is not parcelable. If read from a parcel, the
         * returned OnClickListener represents the first {@link RemoteAction} (if one exists).
         *
         *
         * @deprecated Use {@link #addAction(RemoteAction)} instead.
         * @deprecated Use {@link #addAction(RemoteAction)} instead.
         */
         */
@@ -674,17 +659,7 @@ public final class TextClassification implements Parcelable {
    @Override
    @Override
    public void writeToParcel(Parcel dest, int flags) {
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mText);
        dest.writeString(mText);
        final Bitmap legacyIconBitmap = drawableToBitmap(mLegacyIcon, MAX_LEGACY_ICON_SIZE);
        // NOTE: legacy fields are not parcelled.
        dest.writeInt(legacyIconBitmap != null ? 1 : 0);
        if (legacyIconBitmap != null) {
            legacyIconBitmap.writeToParcel(dest, flags);
        }
        dest.writeString(mLegacyLabel);
        dest.writeInt(mLegacyIntent != null ? 1 : 0);
        if (mLegacyIntent != null) {
            mLegacyIntent.writeToParcel(dest, flags);
        }
        // mOnClickListener is not parcelable.
        dest.writeTypedList(mActions);
        dest.writeTypedList(mActions);
        mEntityConfidence.writeToParcel(dest, flags);
        mEntityConfidence.writeToParcel(dest, flags);
        dest.writeString(mId);
        dest.writeString(mId);
@@ -705,23 +680,43 @@ public final class TextClassification implements Parcelable {


    private TextClassification(Parcel in) {
    private TextClassification(Parcel in) {
        mText = in.readString();
        mText = in.readString();
        mLegacyIcon = in.readInt() == 0
        mActions = in.createTypedArrayList(RemoteAction.CREATOR);
                ? null
        if (!mActions.isEmpty()) {
                : new BitmapDrawable(Resources.getSystem(), Bitmap.CREATOR.createFromParcel(in));
            final RemoteAction action = mActions.get(0);
        mLegacyLabel = in.readString();
            mLegacyIcon = maybeLoadDrawable(action.getIcon());
        if (in.readInt() == 0) {
            mLegacyLabel = action.getTitle().toString();
            mLegacyIntent = null;
            mLegacyOnClickListener = createIntentOnClickListener(mActions.get(0).getActionIntent());
        } else {
        } else {
            mLegacyIntent = Intent.CREATOR.createFromParcel(in);
            mLegacyIcon = null;
            mLegacyIntent.removeFlags(
            mLegacyLabel = null;
                    Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            mLegacyOnClickListener = null;
        }
        }
        mLegacyOnClickListener = null;  // not parcelable
        mLegacyIntent = null; // mLegacyIntent is not parcelled.
        mActions = in.createTypedArrayList(RemoteAction.CREATOR);
        mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
        mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
        mId = in.readString();
        mId = in.readString();
    }
    }


    // Best effort attempt to try to load a drawable from the provided icon.
    @Nullable
    private static Drawable maybeLoadDrawable(Icon icon) {
        if (icon == null) {
            return null;
        }
        switch (icon.getType()) {
            case Icon.TYPE_BITMAP:
                return new BitmapDrawable(Resources.getSystem(), icon.getBitmap());
            case Icon.TYPE_ADAPTIVE_BITMAP:
                return new AdaptiveIconDrawable(null,
                        new BitmapDrawable(Resources.getSystem(), icon.getBitmap()));
            case Icon.TYPE_DATA:
                return new BitmapDrawable(
                        Resources.getSystem(),
                        BitmapFactory.decodeByteArray(
                                icon.getDataBytes(), icon.getDataOffset(), icon.getDataLength()));
        }
        return null;
    }

    // TODO: Remove once apps can build against the latest sdk.
    // TODO: Remove once apps can build against the latest sdk.
    /**
    /**
     * Optional input parameters for generating TextClassification.
     * Optional input parameters for generating TextClassification.
+33 −24
Original line number Original line Diff line number Diff line
@@ -17,6 +17,7 @@
package android.view.textclassifier;
package android.view.textclassifier;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertNull;


import android.app.PendingIntent;
import android.app.PendingIntent;
@@ -26,6 +27,7 @@ import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.Icon;
import android.os.LocaleList;
import android.os.LocaleList;
import android.os.Parcel;
import android.os.Parcel;
@@ -99,12 +101,6 @@ public class TextClassificationTest {
        assertEquals(id, result.getId());
        assertEquals(id, result.getId());
        assertEquals(2, result.getActions().size());
        assertEquals(2, result.getActions().size());


        // Legacy API.
        assertNull(result.getIcon());
        assertNull(result.getLabel());
        assertNull(result.getIntent());
        assertNull(result.getOnClickListener());

        // Primary action.
        // Primary action.
        final RemoteAction primaryAction = result.getActions().get(0);
        final RemoteAction primaryAction = result.getActions().get(0);
        assertEquals(primaryLabel, primaryAction.getTitle());
        assertEquals(primaryLabel, primaryAction.getTitle());
@@ -128,23 +124,35 @@ public class TextClassificationTest {
    @Test
    @Test
    public void testParcelLegacy() {
    public void testParcelLegacy() {
        final Context context = InstrumentationRegistry.getInstrumentation().getContext();
        final Context context = InstrumentationRegistry.getInstrumentation().getContext();
        final String text = "text";


        final Icon icon = generateTestIcon(384, 192, Color.BLUE);
        final int legacyIconWidth = 192;
        final int legacyIconHeight = 96;
        final int legacyIconColor = Color.BLUE;
        final Drawable legacyIcon = generateTestIcon(
                legacyIconWidth, legacyIconHeight, legacyIconColor)
                .loadDrawable(context);
        final String legacyLabel = "legacyLabel";
        final Intent legacyIntent = new Intent("ACTION_LEGACY");
        final View.OnClickListener legacyOnClick = null;

        final int width = 384;
        final int height = 192;
        final int iconColor = Color.RED;
        final String label = "label";
        final String label = "label";
        final Intent intent = new Intent("intent");
        final PendingIntent pendingIntent = PendingIntent.getActivity(
        final View.OnClickListener onClickListener = v -> { };
                context, 0, new Intent("ACTION_0"), 0);
        final RemoteAction remoteAction = new RemoteAction(
                generateTestIcon(width, height, iconColor),
                label,
                "description",
                pendingIntent);


        final String id = "id";
        final TextClassification reference = new TextClassification.Builder()
        final TextClassification reference = new TextClassification.Builder()
                .setText(text)
                .setIcon(legacyIcon)
                .setIcon(icon.loadDrawable(context))
                .setLabel(legacyLabel)
                .setLabel(label)
                .setIntent(legacyIntent)
                .setIntent(intent)
                .setOnClickListener(legacyOnClick)
                .setOnClickListener(onClickListener)
                .addAction(remoteAction)
                .setEntityType(TextClassifier.TYPE_ADDRESS, 0.3f)
                .setEntityType(TextClassifier.TYPE_PHONE, 0.7f)
                .setId(id)
                .build();
                .build();


        // Parcel and unparcel
        // Parcel and unparcel
@@ -153,13 +161,14 @@ public class TextClassificationTest {
        parcel.setDataPosition(0);
        parcel.setDataPosition(0);
        final TextClassification result = TextClassification.CREATOR.createFromParcel(parcel);
        final TextClassification result = TextClassification.CREATOR.createFromParcel(parcel);


        // Legacy fields excluding legacyIntent are replaced by first remoteAction.
        assertNull(result.getIntent());
        final Bitmap resultIcon = ((BitmapDrawable) result.getIcon()).getBitmap();
        final Bitmap resultIcon = ((BitmapDrawable) result.getIcon()).getBitmap();
        assertEquals(icon.getBitmap().getPixel(0, 0), resultIcon.getPixel(0, 0));
        assertEquals(iconColor, resultIcon.getPixel(0, 0));
        assertEquals(192, resultIcon.getWidth());
        assertEquals(width, resultIcon.getWidth());
        assertEquals(96, resultIcon.getHeight());
        assertEquals(height, resultIcon.getHeight());
        assertEquals(label, result.getLabel());
        assertEquals(label, result.getLabel());
        assertEquals(intent.getAction(), result.getIntent().getAction());
        assertNotNull(result.getOnClickListener());
        assertNull(result.getOnClickListener());
    }
    }


    @Test
    @Test