ImageTransformation.java revision 7fc29dd9311cc36c3eb2a6a05aeed2d39ddcc604
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, "^4815.*$", R.drawable.ic_credit_card_logo1)
46 *     .addOption("^1623.*$", R.drawable.ic_credit_card_logo2)
47 *     .addOption("^42.*$", R.drawable.ic_credit_card_logo3)
48 *     .build();
49 * </pre>
50 *
51 * <p>There is no imposed limit in the number of options, but keep in mind that regexs are
52 * expensive to evaluate, so use the minimum number of regexs and add the most common first
53 * (for example, if this is a tranformation for a credit card logo and the most common credit card
54 * issuers are banks X and Y, add the regexes that resolves these 2 banks first).
55 */
56public final class ImageTransformation extends InternalTransformation implements Transformation,
57        Parcelable {
58    private static final String TAG = "ImageTransformation";
59
60    private final AutofillId mId;
61    private final ArrayList<Pair<Pattern, Integer>> mOptions;
62
63    private ImageTransformation(Builder builder) {
64        mId = builder.mId;
65        mOptions = builder.mOptions;
66    }
67
68    /** @hide */
69    @TestApi
70    @Override
71    public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
72            int childViewId) {
73        final String value = finder.findByAutofillId(mId);
74        if (value == null) {
75            Log.w(TAG, "No view for id " + mId);
76            return;
77        }
78        final int size = mOptions.size();
79        if (sDebug) {
80            Log.d(TAG, size + " multiple options on id " + childViewId + " to compare against "
81                    + value);
82        }
83
84        for (int i = 0; i < size; i++) {
85            Pair<Pattern, Integer> regex = mOptions.get(i);
86            if (regex.first.matcher(value).matches()) {
87                Log.d(TAG, "Found match at " + i + ": " + regex);
88                parentTemplate.setImageViewResource(childViewId, regex.second);
89                return;
90            }
91        }
92        Log.w(TAG, "No match for " + value);
93    }
94
95    /**
96     * Builder for {@link ImageTransformation} objects.
97     */
98    public static class Builder {
99        private final AutofillId mId;
100        private final ArrayList<Pair<Pattern, Integer>> mOptions = new ArrayList<>();
101        private boolean mDestroyed;
102
103        /**
104         * Create a new builder for a autofill id and add a first option.
105         *
106         * @param id id of the screen field that will be used to evaluate whether the image should
107         * be used.
108         * @param regex regular expression defining what should be matched to use this image. The
109         * pattern will be {@link Pattern#compile compiled} without setting any flags.
110         * @param resId resource id of the image (in the autofill service's package). The
111         * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
112         */
113        public Builder(@NonNull AutofillId id, @NonNull String regex, @DrawableRes int resId) {
114            mId = Preconditions.checkNotNull(id);
115
116            addOption(regex, resId);
117        }
118
119        /**
120         * Adds an option to replace the child view with a different image when the regex matches.
121         *
122         * @param regex regular expression defining what should be matched to use this image. The
123         * pattern will be {@link Pattern#compile compiled} without setting any flags.
124         * @param resId resource id of the image (in the autofill service's package). The
125         * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
126         *
127         * @return this build
128         */
129        public Builder addOption(@NonNull String regex, @DrawableRes int resId) {
130            throwIfDestroyed();
131
132            Preconditions.checkArgument(resId != 0);
133
134            // Check regex
135            Pattern pattern = Pattern.compile(regex);
136
137            mOptions.add(new Pair<>(pattern, resId));
138            return this;
139        }
140
141        /**
142         * Creates a new {@link ImageTransformation} instance.
143         *
144         * @throws IllegalStateException if no call to {@link #addOption(String, int)} was made.
145         */
146        public ImageTransformation build() {
147            throwIfDestroyed();
148            Preconditions.checkState(mOptions != null && !mOptions.isEmpty(),
149                    "Must add at least one option");
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 String[] regexs = new String[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.pattern();
186            resIds[i] = regex.second;
187        }
188        parcel.writeStringArray(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 String[] regexs = parcel.createStringArray();
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