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.DrawableRes;
     22 import android.annotation.NonNull;
     23 import android.annotation.Nullable;
     24 import android.annotation.TestApi;
     25 import android.os.Parcel;
     26 import android.os.Parcelable;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 import android.view.autofill.AutofillId;
     30 import android.widget.ImageView;
     31 import android.widget.RemoteViews;
     32 
     33 import com.android.internal.util.Preconditions;
     34 
     35 import java.util.ArrayList;
     36 import java.util.regex.Pattern;
     37 
     38 /**
     39  * Replaces the content of a child {@link ImageView} of a
     40  * {@link RemoteViews presentation template} with the first image that matches a regular expression
     41  * (regex).
     42  *
     43  * <p>Typically used to display credit card logos. Example:
     44  *
     45  * <pre class="prettyprint">
     46  *   new ImageTransformation.Builder(ccNumberId, Pattern.compile("^4815.*$"),
     47  *                                   R.drawable.ic_credit_card_logo1, "Brand 1")
     48  *     .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2, "Brand 2")
     49  *     .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3, "Brand 3")
     50  *     .build();
     51  * </pre>
     52  *
     53  * <p>There is no imposed limit in the number of options, but keep in mind that regexs are
     54  * expensive to evaluate, so use the minimum number of regexs and add the most common first
     55  * (for example, if this is a tranformation for a credit card logo and the most common credit card
     56  * issuers are banks X and Y, add the regexes that resolves these 2 banks first).
     57  */
     58 public final class ImageTransformation extends InternalTransformation implements Transformation,
     59         Parcelable {
     60     private static final String TAG = "ImageTransformation";
     61 
     62     private final AutofillId mId;
     63     private final ArrayList<Option> mOptions;
     64 
     65     private ImageTransformation(Builder builder) {
     66         mId = builder.mId;
     67         mOptions = builder.mOptions;
     68     }
     69 
     70     /** @hide */
     71     @TestApi
     72     @Override
     73     public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
     74             int childViewId) throws Exception {
     75         final String value = finder.findByAutofillId(mId);
     76         if (value == null) {
     77             Log.w(TAG, "No view for id " + mId);
     78             return;
     79         }
     80         final int size = mOptions.size();
     81         if (sDebug) {
     82             Log.d(TAG, size + " multiple options on id " + childViewId + " to compare against");
     83         }
     84 
     85         for (int i = 0; i < size; i++) {
     86             final Option option = mOptions.get(i);
     87             try {
     88                 if (option.pattern.matcher(value).matches()) {
     89                     Log.d(TAG, "Found match at " + i + ": " + option);
     90                     parentTemplate.setImageViewResource(childViewId, option.resId);
     91                     if (option.contentDescription != null) {
     92                         parentTemplate.setContentDescription(childViewId,
     93                                 option.contentDescription);
     94                     }
     95                     return;
     96                 }
     97             } catch (Exception e) {
     98                 // Do not log full exception to avoid PII leaking
     99                 Log.w(TAG, "Error matching regex #" + i + "(" + option.pattern + ") on id "
    100                         + option.resId + ": " + e.getClass());
    101                 throw e;
    102 
    103             }
    104         }
    105         if (sDebug) Log.d(TAG, "No match for " + value);
    106     }
    107 
    108     /**
    109      * Builder for {@link ImageTransformation} objects.
    110      */
    111     public static class Builder {
    112         private final AutofillId mId;
    113         private final ArrayList<Option> mOptions = new ArrayList<>();
    114         private boolean mDestroyed;
    115 
    116         /**
    117          * Creates a new builder for a autofill id and add a first option.
    118          *
    119          * @param id id of the screen field that will be used to evaluate whether the image should
    120          * be used.
    121          * @param regex regular expression defining what should be matched to use this image.
    122          * @param resId resource id of the image (in the autofill service's package). The
    123          * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
    124          *
    125          * @deprecated use
    126          * {@link #ImageTransformation.Builder(AutofillId, Pattern, int, CharSequence)} instead.
    127          */
    128         @Deprecated
    129         public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId) {
    130             mId = Preconditions.checkNotNull(id);
    131             addOption(regex, resId);
    132         }
    133 
    134         /**
    135          * Creates a new builder for a autofill id and add a first option.
    136          *
    137          * @param id id of the screen field that will be used to evaluate whether the image should
    138          * be used.
    139          * @param regex regular expression defining what should be matched to use this image.
    140          * @param resId resource id of the image (in the autofill service's package). The
    141          * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
    142          * @param contentDescription content description to be applied in the child view.
    143          */
    144         public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId,
    145                 @NonNull CharSequence contentDescription) {
    146             mId = Preconditions.checkNotNull(id);
    147             addOption(regex, resId, contentDescription);
    148         }
    149 
    150         /**
    151          * Adds an option to replace the child view with a different image when the regex matches.
    152          *
    153          * @param regex regular expression defining what should be matched to use this image.
    154          * @param resId resource id of the image (in the autofill service's package). The
    155          * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
    156          *
    157          * @return this build
    158          *
    159          * @deprecated use {@link #addOption(Pattern, int, CharSequence)} instead.
    160          */
    161         @Deprecated
    162         public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId) {
    163             addOptionInternal(regex, resId, null);
    164             return this;
    165         }
    166 
    167         /**
    168          * Adds an option to replace the child view with a different image and content description
    169          * when the regex matches.
    170          *
    171          * @param regex regular expression defining what should be matched to use this image.
    172          * @param resId resource id of the image (in the autofill service's package). The
    173          * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
    174          * @param contentDescription content description to be applied in the child view.
    175          *
    176          * @return this build
    177          */
    178         public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId,
    179                 @NonNull CharSequence contentDescription) {
    180             addOptionInternal(regex, resId, Preconditions.checkNotNull(contentDescription));
    181             return this;
    182         }
    183 
    184         private void addOptionInternal(@NonNull Pattern regex, @DrawableRes int resId,
    185                 @Nullable CharSequence contentDescription) {
    186             throwIfDestroyed();
    187 
    188             Preconditions.checkNotNull(regex);
    189             Preconditions.checkArgument(resId != 0);
    190 
    191             mOptions.add(new Option(regex, resId, contentDescription));
    192         }
    193 
    194 
    195         /**
    196          * Creates a new {@link ImageTransformation} instance.
    197          */
    198         public ImageTransformation build() {
    199             throwIfDestroyed();
    200             mDestroyed = true;
    201             return new ImageTransformation(this);
    202         }
    203 
    204         private void throwIfDestroyed() {
    205             Preconditions.checkState(!mDestroyed, "Already called build()");
    206         }
    207     }
    208 
    209     /////////////////////////////////////
    210     // Object "contract" methods. //
    211     /////////////////////////////////////
    212     @Override
    213     public String toString() {
    214         if (!sDebug) return super.toString();
    215 
    216         return "ImageTransformation: [id=" + mId + ", options=" + mOptions + "]";
    217     }
    218 
    219     /////////////////////////////////////
    220     // Parcelable "contract" methods. //
    221     /////////////////////////////////////
    222     @Override
    223     public int describeContents() {
    224         return 0;
    225     }
    226     @Override
    227     public void writeToParcel(Parcel parcel, int flags) {
    228         parcel.writeParcelable(mId, flags);
    229 
    230         final int size = mOptions.size();
    231         final Pattern[] patterns = new Pattern[size];
    232         final int[] resIds = new int[size];
    233         final CharSequence[] contentDescriptions = new String[size];
    234         for (int i = 0; i < size; i++) {
    235             final Option option = mOptions.get(i);
    236             patterns[i] = option.pattern;
    237             resIds[i] = option.resId;
    238             contentDescriptions[i] = option.contentDescription;
    239         }
    240         parcel.writeSerializable(patterns);
    241         parcel.writeIntArray(resIds);
    242         parcel.writeCharSequenceArray(contentDescriptions);
    243     }
    244 
    245     public static final Parcelable.Creator<ImageTransformation> CREATOR =
    246             new Parcelable.Creator<ImageTransformation>() {
    247         @Override
    248         public ImageTransformation createFromParcel(Parcel parcel) {
    249             final AutofillId id = parcel.readParcelable(null);
    250 
    251             final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
    252             final int[] resIds = parcel.createIntArray();
    253             final CharSequence[] contentDescriptions = parcel.readCharSequenceArray();
    254 
    255             // Always go through the builder to ensure the data ingested by the system obeys the
    256             // contract of the builder to avoid attacks using specially crafted parcels.
    257             final CharSequence contentDescription = contentDescriptions[0];
    258             final ImageTransformation.Builder builder = (contentDescription != null)
    259                     ? new ImageTransformation.Builder(id, regexs[0], resIds[0], contentDescription)
    260                     : new ImageTransformation.Builder(id, regexs[0], resIds[0]);
    261 
    262             final int size = regexs.length;
    263             for (int i = 1; i < size; i++) {
    264                 if (contentDescriptions[i] != null) {
    265                     builder.addOption(regexs[i], resIds[i], contentDescriptions[i]);
    266                 } else {
    267                     builder.addOption(regexs[i], resIds[i]);
    268                 }
    269             }
    270 
    271             return builder.build();
    272         }
    273 
    274         @Override
    275         public ImageTransformation[] newArray(int size) {
    276             return new ImageTransformation[size];
    277         }
    278     };
    279 
    280     private static final class Option {
    281         public final Pattern pattern;
    282         public final int resId;
    283         public final CharSequence contentDescription;
    284 
    285         Option(Pattern pattern, int resId, CharSequence contentDescription) {
    286             this.pattern = pattern;
    287             this.resId = resId;
    288             this.contentDescription = TextUtils.trimNoCopySpans(contentDescription);
    289         }
    290     }
    291 }
    292