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