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.NonNull;
22import android.annotation.Nullable;
23import android.app.Activity;
24import android.app.PendingIntent;
25import android.os.Parcel;
26import android.os.Parcelable;
27import android.util.Pair;
28import android.widget.RemoteViews;
29
30import com.android.internal.util.Preconditions;
31
32import 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 */
88public 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