Home | History | Annotate | Download | only in autofill
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.service.autofill;
     18 
     19 import static android.view.autofill.Helper.sDebug;
     20 
     21 import android.annotation.NonNull;
     22 import android.annotation.Nullable;
     23 import android.app.Activity;
     24 import android.app.PendingIntent;
     25 import android.os.Parcel;
     26 import android.os.Parcelable;
     27 import android.util.Pair;
     28 import android.widget.RemoteViews;
     29 
     30 import com.android.internal.util.Preconditions;
     31 
     32 import java.util.ArrayList;
     33 
     34 /**
     35  * Defines a custom description for the autofill save UI.
     36  *
     37  * <p>This is useful when the autofill service needs to show a detailed view of what would be saved;
     38  * for example, when the screen contains a credit card, it could display a logo of the credit card
     39  * bank, the last four digits of the credit card number, and its expiration number.
     40  *
     41  * <p>A custom description is made of 2 parts:
     42  * <ul>
     43  *   <li>A {@link RemoteViews presentation template} containing children views.
     44  *   <li>{@link Transformation Transformations} to populate the children views.
     45  * </ul>
     46  *
     47  * <p>For the credit card example mentioned above, the (simplified) template would be:
     48  *
     49  * <pre class="prettyprint">
     50  * &lt;LinearLayout&gt;
     51  *   &lt;ImageView android:id="@+id/templateccLogo"/&gt;
     52  *   &lt;TextView android:id="@+id/templateCcNumber"/&gt;
     53  *   &lt;TextView android:id="@+id/templateExpDate"/&gt;
     54  * &lt;/LinearLayout&gt;
     55  * </pre>
     56  *
     57  * <p>Which in code translates to:
     58  *
     59  * <pre class="prettyprint">
     60  *   CustomDescription.Builder buider = new Builder(new RemoteViews(pgkName, R.layout.cc_template);
     61  * </pre>
     62  *
     63  * <p>Then the value of each of the 3 children would be changed at runtime based on the the value of
     64  * the screen fields and the {@link Transformation Transformations}:
     65  *
     66  * <pre class="prettyprint">
     67  * // Image child - different logo for each bank, based on credit card prefix
     68  * builder.addChild(R.id.templateccLogo,
     69  *   new ImageTransformation.Builder(ccNumberId)
     70  *     .addOption(Pattern.compile("^4815.*$"), R.drawable.ic_credit_card_logo1)
     71  *     .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2)
     72  *     .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3)
     73  *     .build();
     74  * // Masked credit card number (as .....LAST_4_DIGITS)
     75  * builder.addChild(R.id.templateCcNumber, new CharSequenceTransformation
     76  *     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
     77  *     .build();
     78  * // Expiration date as MM / YYYY:
     79  * builder.addChild(R.id.templateExpDate, new CharSequenceTransformation
     80  *     .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
     81  *     .addField(ccExpYearId, Pattern.compile("^(\\d\\d)$"), "/$1")
     82  *     .build();
     83  * </pre>
     84  *
     85  * <p>See {@link ImageTransformation}, {@link CharSequenceTransformation} for more info about these
     86  * transformations.
     87  */
     88 public final class CustomDescription implements Parcelable {
     89 
     90     private final RemoteViews mPresentation;
     91     private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
     92     private final ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates;
     93 
     94     private CustomDescription(Builder builder) {
     95         mPresentation = builder.mPresentation;
     96         mTransformations = builder.mTransformations;
     97         mUpdates = builder.mUpdates;
     98     }
     99 
    100     /** @hide */
    101     @Nullable
    102     public RemoteViews getPresentation() {
    103         return mPresentation;
    104     }
    105 
    106     /** @hide */
    107     @Nullable
    108     public ArrayList<Pair<Integer, InternalTransformation>> getTransformations() {
    109         return mTransformations;
    110     }
    111 
    112     /** @hide */
    113     @Nullable
    114     public ArrayList<Pair<InternalValidator, BatchUpdates>> getUpdates() {
    115         return mUpdates;
    116     }
    117 
    118     /**
    119      * Builder for {@link CustomDescription} objects.
    120      */
    121     public static class Builder {
    122         private final RemoteViews mPresentation;
    123 
    124         private boolean mDestroyed;
    125         private ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
    126         private ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates;
    127 
    128         /**
    129          * Default constructor.
    130          *
    131          * <p><b>Note:</b> If any child view of presentation triggers a
    132          * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent) pending intent
    133          * on click}, such {@link PendingIntent} must follow the restrictions below, otherwise
    134          * it might not be triggered or the autofill save UI might not be shown when its activity
    135          * is finished:
    136          * <ul>
    137          *   <li>It cannot be created with the {@link PendingIntent#FLAG_IMMUTABLE} flag.
    138          *   <li>It must be a PendingIntent for an {@link Activity}.
    139          *   <li>The activity must call {@link Activity#finish()} when done.
    140          *   <li>The activity should not launch other activities.
    141          * </ul>
    142          *
    143          * @param parentPresentation template presentation with (optional) children views.
    144          * @throws NullPointerException if {@code parentPresentation} is null (on Android
    145          * {@link android.os.Build.VERSION_CODES#P} or higher).
    146          */
    147         public Builder(@NonNull RemoteViews parentPresentation) {
    148             mPresentation = Preconditions.checkNotNull(parentPresentation);
    149         }
    150 
    151         /**
    152          * Adds a transformation to replace the value of a child view with the fields in the
    153          * screen.
    154          *
    155          * <p>When multiple transformations are added for the same child view, they will be applied
    156          * in the same order as added.
    157          *
    158          * @param id view id of the children view.
    159          * @param transformation an implementation provided by the Android System.
    160          * @return this builder.
    161          * @throws IllegalArgumentException if {@code transformation} is not a class provided
    162          * by the Android System.
    163          */
    164         public Builder addChild(int id, @NonNull Transformation transformation) {
    165             throwIfDestroyed();
    166             Preconditions.checkArgument((transformation instanceof InternalTransformation),
    167                     "not provided by Android System: " + transformation);
    168             if (mTransformations == null) {
    169                 mTransformations = new ArrayList<>();
    170             }
    171             mTransformations.add(new Pair<>(id, (InternalTransformation) transformation));
    172             return this;
    173         }
    174 
    175         /**
    176          * Updates the {@link RemoteViews presentation template} when a condition is satisfied by
    177          * applying a series of remote view operations. This allows dynamic customization of the
    178          * portion of the save UI that is controlled by the autofill service. Such dynamic
    179          * customization is based on the content of target views.
    180          *
    181          * <p>The updates are applied in the sequence they are added, after the
    182          * {@link #addChild(int, Transformation) transformations} are applied to the children
    183          * views.
    184          *
    185          * <p>For example, to make children views visible when fields are not empty:
    186          *
    187          * <pre class="prettyprint">
    188          * RemoteViews template = new RemoteViews(pgkName, R.layout.my_full_template);
    189          *
    190          * Pattern notEmptyPattern = Pattern.compile(".+");
    191          * Validator hasAddress = new RegexValidator(addressAutofillId, notEmptyPattern);
    192          * Validator hasCcNumber = new RegexValidator(ccNumberAutofillId, notEmptyPattern);
    193          *
    194          * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_full_template)
    195          * addressUpdates.setViewVisibility(R.id.address, View.VISIBLE);
    196          *
    197          * // Make address visible
    198          * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder()
    199          *     .updateTemplate(addressUpdates)
    200          *     .build();
    201          *
    202          * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_full_template)
    203          * ccUpdates.setViewVisibility(R.id.cc_number, View.VISIBLE);
    204          *
    205          * // Mask credit card number (as .....LAST_4_DIGITS) and make it visible
    206          * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder()
    207          *     .updateTemplate(ccUpdates)
    208          *     .transformChild(R.id.templateCcNumber, new CharSequenceTransformation
    209          *                     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
    210          *                     .build())
    211          *     .build();
    212          *
    213          * CustomDescription customDescription = new CustomDescription.Builder(template)
    214          *     .batchUpdate(hasAddress, addressBatchUpdates)
    215          *     .batchUpdate(hasCcNumber, ccBatchUpdates)
    216          *     .build();
    217          * </pre>
    218          *
    219          * <p>Another approach is to add a child first, then apply the transformations. Example:
    220          *
    221          * <pre class="prettyprint">
    222          * RemoteViews template = new RemoteViews(pgkName, R.layout.my_base_template);
    223          *
    224          * RemoteViews addressPresentation = new RemoteViews(pgkName, R.layout.address)
    225          * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_template)
    226          * addressUpdates.addView(R.id.parentId, addressPresentation);
    227          * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder()
    228          *     .updateTemplate(addressUpdates)
    229          *     .build();
    230          *
    231          * RemoteViews ccPresentation = new RemoteViews(pgkName, R.layout.cc)
    232          * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_template)
    233          * ccUpdates.addView(R.id.parentId, ccPresentation);
    234          * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder()
    235          *     .updateTemplate(ccUpdates)
    236          *     .transformChild(R.id.templateCcNumber, new CharSequenceTransformation
    237          *                     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
    238          *                     .build())
    239          *     .build();
    240          *
    241          * CustomDescription customDescription = new CustomDescription.Builder(template)
    242          *     .batchUpdate(hasAddress, addressBatchUpdates)
    243          *     .batchUpdate(hasCcNumber, ccBatchUpdates)
    244          *     .build();
    245          * </pre>
    246          *
    247          * @param condition condition used to trigger the updates.
    248          * @param updates actions to be applied to the
    249          * {@link #CustomDescription.Builder(RemoteViews) template presentation} when the condition
    250          * is satisfied.
    251          *
    252          * @return this builder
    253          * @throws IllegalArgumentException if {@code condition} is not a class provided
    254          * by the Android System.
    255          */
    256         public Builder batchUpdate(@NonNull Validator condition, @NonNull BatchUpdates updates) {
    257             throwIfDestroyed();
    258             Preconditions.checkArgument((condition instanceof InternalValidator),
    259                     "not provided by Android System: " + condition);
    260             Preconditions.checkNotNull(updates);
    261             if (mUpdates == null) {
    262                 mUpdates = new ArrayList<>();
    263             }
    264             mUpdates.add(new Pair<>((InternalValidator) condition, updates));
    265             return this;
    266         }
    267 
    268         /**
    269          * Creates a new {@link CustomDescription} instance.
    270          */
    271         public CustomDescription build() {
    272             throwIfDestroyed();
    273             mDestroyed = true;
    274             return new CustomDescription(this);
    275         }
    276 
    277         private void throwIfDestroyed() {
    278             if (mDestroyed) {
    279                 throw new IllegalStateException("Already called #build()");
    280             }
    281         }
    282     }
    283 
    284     /////////////////////////////////////
    285     // Object "contract" methods. //
    286     /////////////////////////////////////
    287     @Override
    288     public String toString() {
    289         if (!sDebug) return super.toString();
    290 
    291         return new StringBuilder("CustomDescription: [presentation=")
    292                 .append(mPresentation)
    293                 .append(", transformations=")
    294                     .append(mTransformations == null ? "N/A" : mTransformations.size())
    295                 .append(", updates=")
    296                     .append(mUpdates == null ? "N/A" : mUpdates.size())
    297                 .append("]").toString();
    298     }
    299 
    300     /////////////////////////////////////
    301     // Parcelable "contract" methods. //
    302     /////////////////////////////////////
    303     @Override
    304     public int describeContents() {
    305         return 0;
    306     }
    307 
    308     @Override
    309     public void writeToParcel(Parcel dest, int flags) {
    310         dest.writeParcelable(mPresentation, flags);
    311         if (mPresentation == null) return;
    312 
    313         if (mTransformations == null) {
    314             dest.writeIntArray(null);
    315         } else {
    316             final int size = mTransformations.size();
    317             final int[] ids = new int[size];
    318             final InternalTransformation[] values = new InternalTransformation[size];
    319             for (int i = 0; i < size; i++) {
    320                 final Pair<Integer, InternalTransformation> pair = mTransformations.get(i);
    321                 ids[i] = pair.first;
    322                 values[i] = pair.second;
    323             }
    324             dest.writeIntArray(ids);
    325             dest.writeParcelableArray(values, flags);
    326         }
    327         if (mUpdates == null) {
    328             dest.writeParcelableArray(null, flags);
    329         } else {
    330             final int size = mUpdates.size();
    331             final InternalValidator[] conditions = new InternalValidator[size];
    332             final BatchUpdates[] updates = new BatchUpdates[size];
    333 
    334             for (int i = 0; i < size; i++) {
    335                 final Pair<InternalValidator, BatchUpdates> pair = mUpdates.get(i);
    336                 conditions[i] = pair.first;
    337                 updates[i] = pair.second;
    338             }
    339             dest.writeParcelableArray(conditions, flags);
    340             dest.writeParcelableArray(updates, flags);
    341         }
    342     }
    343     public static final Parcelable.Creator<CustomDescription> CREATOR =
    344             new Parcelable.Creator<CustomDescription>() {
    345         @Override
    346         public CustomDescription createFromParcel(Parcel parcel) {
    347             // Always go through the builder to ensure the data ingested by
    348             // the system obeys the contract of the builder to avoid attacks
    349             // using specially crafted parcels.
    350             final RemoteViews parentPresentation = parcel.readParcelable(null);
    351             if (parentPresentation == null) return null;
    352 
    353             final Builder builder = new Builder(parentPresentation);
    354             final int[] ids = parcel.createIntArray();
    355             if (ids != null) {
    356                 final InternalTransformation[] values =
    357                     parcel.readParcelableArray(null, InternalTransformation.class);
    358                 final int size = ids.length;
    359                 for (int i = 0; i < size; i++) {
    360                     builder.addChild(ids[i], values[i]);
    361                 }
    362             }
    363             final InternalValidator[] conditions =
    364                     parcel.readParcelableArray(null, InternalValidator.class);
    365             if (conditions != null) {
    366                 final BatchUpdates[] updates = parcel.readParcelableArray(null, BatchUpdates.class);
    367                 final int size = conditions.length;
    368                 for (int i = 0; i < size; i++) {
    369                     builder.batchUpdate(conditions[i], updates[i]);
    370                 }
    371             }
    372             return builder.build();
    373         }
    374 
    375         @Override
    376         public CustomDescription[] newArray(int size) {
    377             return new CustomDescription[size];
    378         }
    379     };
    380 }
    381