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.annotation.TestApi;
24 import android.app.Activity;
25 import android.app.PendingIntent;
26 import android.os.Parcel;
27 import android.os.Parcelable;
28 import android.util.Pair;
29 import android.util.SparseArray;
30 import android.widget.RemoteViews;
31 
32 import com.android.internal.util.Preconditions;
33 
34 import java.util.ArrayList;
35 import java.util.Objects;
36 
37 /**
38  * Defines a custom description for the autofill save UI.
39  *
40  * <p>This is useful when the autofill service needs to show a detailed view of what would be saved;
41  * for example, when the screen contains a credit card, it could display a logo of the credit card
42  * bank, the last four digits of the credit card number, and its expiration number.
43  *
44  * <p>A custom description is made of 2 parts:
45  * <ul>
46  *   <li>A {@link RemoteViews presentation template} containing children views.
47  *   <li>{@link Transformation Transformations} to populate the children views.
48  * </ul>
49  *
50  * <p>For the credit card example mentioned above, the (simplified) template would be:
51  *
52  * <pre class="prettyprint">
53  * &lt;LinearLayout&gt;
54  *   &lt;ImageView android:id="@+id/templateccLogo"/&gt;
55  *   &lt;TextView android:id="@+id/templateCcNumber"/&gt;
56  *   &lt;TextView android:id="@+id/templateExpDate"/&gt;
57  * &lt;/LinearLayout&gt;
58  * </pre>
59  *
60  * <p>Which in code translates to:
61  *
62  * <pre class="prettyprint">
63  *   CustomDescription.Builder buider = new Builder(new RemoteViews(pgkName, R.layout.cc_template);
64  * </pre>
65  *
66  * <p>Then the value of each of the 3 children would be changed at runtime based on the the value of
67  * the screen fields and the {@link Transformation Transformations}:
68  *
69  * <pre class="prettyprint">
70  * // Image child - different logo for each bank, based on credit card prefix
71  * builder.addChild(R.id.templateccLogo,
72  *   new ImageTransformation.Builder(ccNumberId)
73  *     .addOption(Pattern.compile("^4815.*$"), R.drawable.ic_credit_card_logo1)
74  *     .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2)
75  *     .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3)
76  *     .build();
77  * // Masked credit card number (as .....LAST_4_DIGITS)
78  * builder.addChild(R.id.templateCcNumber, new CharSequenceTransformation
79  *     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
80  *     .build();
81  * // Expiration date as MM / YYYY:
82  * builder.addChild(R.id.templateExpDate, new CharSequenceTransformation
83  *     .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
84  *     .addField(ccExpYearId, Pattern.compile("^(\\d\\d)$"), "/$1")
85  *     .build();
86  * </pre>
87  *
88  * <p>See {@link ImageTransformation}, {@link CharSequenceTransformation} for more info about these
89  * transformations.
90  */
91 public final class CustomDescription implements Parcelable {
92 
93     private final RemoteViews mPresentation;
94     private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
95     private final ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates;
96     private final SparseArray<InternalOnClickAction> mActions;
97 
CustomDescription(Builder builder)98     private CustomDescription(Builder builder) {
99         mPresentation = builder.mPresentation;
100         mTransformations = builder.mTransformations;
101         mUpdates = builder.mUpdates;
102         mActions = builder.mActions;
103     }
104 
105     /** @hide */
106     @Nullable
getPresentation()107     public RemoteViews getPresentation() {
108         return mPresentation;
109     }
110 
111     /** @hide */
112     @Nullable
getTransformations()113     public ArrayList<Pair<Integer, InternalTransformation>> getTransformations() {
114         return mTransformations;
115     }
116 
117     /** @hide */
118     @Nullable
getUpdates()119     public ArrayList<Pair<InternalValidator, BatchUpdates>> getUpdates() {
120         return mUpdates;
121     }
122 
123     /** @hide */
124     @Nullable
125     @TestApi
getActions()126     public SparseArray<InternalOnClickAction> getActions() {
127         return mActions;
128     }
129 
130     /**
131      * Builder for {@link CustomDescription} objects.
132      */
133     public static class Builder {
134         private final RemoteViews mPresentation;
135 
136         private boolean mDestroyed;
137         private ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
138         private ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates;
139         private SparseArray<InternalOnClickAction> mActions;
140 
141         /**
142          * Default constructor.
143          *
144          * <p><b>Note:</b> If any child view of presentation triggers a
145          * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent) pending intent
146          * on click}, such {@link PendingIntent} must follow the restrictions below, otherwise
147          * it might not be triggered or the autofill save UI might not be shown when its activity
148          * is finished:
149          * <ul>
150          *   <li>It cannot be created with the {@link PendingIntent#FLAG_IMMUTABLE} flag.
151          *   <li>It must be a PendingIntent for an {@link Activity}.
152          *   <li>The activity must call {@link Activity#finish()} when done.
153          *   <li>The activity should not launch other activities.
154          * </ul>
155          *
156          * @param parentPresentation template presentation with (optional) children views.
157          * @throws NullPointerException if {@code parentPresentation} is null (on Android
158          * {@link android.os.Build.VERSION_CODES#P} or higher).
159          */
Builder(@onNull RemoteViews parentPresentation)160         public Builder(@NonNull RemoteViews parentPresentation) {
161             mPresentation = Objects.requireNonNull(parentPresentation);
162         }
163 
164         /**
165          * Adds a transformation to replace the value of a child view with the fields in the
166          * screen.
167          *
168          * <p>When multiple transformations are added for the same child view, they will be applied
169          * in the same order as added.
170          *
171          * @param id view id of the children view.
172          * @param transformation an implementation provided by the Android System.
173          *
174          * @return this builder.
175          *
176          * @throws IllegalArgumentException if {@code transformation} is not a class provided
177          * by the Android System.
178          * @throws IllegalStateException if {@link #build()} was already called.
179          */
180         @NonNull
addChild(int id, @NonNull Transformation transformation)181         public Builder addChild(int id, @NonNull Transformation transformation) {
182             throwIfDestroyed();
183             Preconditions.checkArgument((transformation instanceof InternalTransformation),
184                     "not provided by Android System: %s", transformation);
185             if (mTransformations == null) {
186                 mTransformations = new ArrayList<>();
187             }
188             mTransformations.add(new Pair<>(id, (InternalTransformation) transformation));
189             return this;
190         }
191 
192         /**
193          * Updates the {@link RemoteViews presentation template} when a condition is satisfied by
194          * applying a series of remote view operations. This allows dynamic customization of the
195          * portion of the save UI that is controlled by the autofill service. Such dynamic
196          * customization is based on the content of target views.
197          *
198          * <p>The updates are applied in the sequence they are added, after the
199          * {@link #addChild(int, Transformation) transformations} are applied to the children
200          * views.
201          *
202          * <p>For example, to make children views visible when fields are not empty:
203          *
204          * <pre class="prettyprint">
205          * RemoteViews template = new RemoteViews(pgkName, R.layout.my_full_template);
206          *
207          * Pattern notEmptyPattern = Pattern.compile(".+");
208          * Validator hasAddress = new RegexValidator(addressAutofillId, notEmptyPattern);
209          * Validator hasCcNumber = new RegexValidator(ccNumberAutofillId, notEmptyPattern);
210          *
211          * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_full_template)
212          * addressUpdates.setViewVisibility(R.id.address, View.VISIBLE);
213          *
214          * // Make address visible
215          * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder()
216          *     .updateTemplate(addressUpdates)
217          *     .build();
218          *
219          * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_full_template)
220          * ccUpdates.setViewVisibility(R.id.cc_number, View.VISIBLE);
221          *
222          * // Mask credit card number (as .....LAST_4_DIGITS) and make it visible
223          * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder()
224          *     .updateTemplate(ccUpdates)
225          *     .transformChild(R.id.templateCcNumber, new CharSequenceTransformation
226          *                     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
227          *                     .build())
228          *     .build();
229          *
230          * CustomDescription customDescription = new CustomDescription.Builder(template)
231          *     .batchUpdate(hasAddress, addressBatchUpdates)
232          *     .batchUpdate(hasCcNumber, ccBatchUpdates)
233          *     .build();
234          * </pre>
235          *
236          * <p>Another approach is to add a child first, then apply the transformations. Example:
237          *
238          * <pre class="prettyprint">
239          * RemoteViews template = new RemoteViews(pgkName, R.layout.my_base_template);
240          *
241          * RemoteViews addressPresentation = new RemoteViews(pgkName, R.layout.address)
242          * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_template)
243          * addressUpdates.addView(R.id.parentId, addressPresentation);
244          * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder()
245          *     .updateTemplate(addressUpdates)
246          *     .build();
247          *
248          * RemoteViews ccPresentation = new RemoteViews(pgkName, R.layout.cc)
249          * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_template)
250          * ccUpdates.addView(R.id.parentId, ccPresentation);
251          * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder()
252          *     .updateTemplate(ccUpdates)
253          *     .transformChild(R.id.templateCcNumber, new CharSequenceTransformation
254          *                     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
255          *                     .build())
256          *     .build();
257          *
258          * CustomDescription customDescription = new CustomDescription.Builder(template)
259          *     .batchUpdate(hasAddress, addressBatchUpdates)
260          *     .batchUpdate(hasCcNumber, ccBatchUpdates)
261          *     .build();
262          * </pre>
263          *
264          * @param condition condition used to trigger the updates.
265          * @param updates actions to be applied to the
266          * {@link #Builder(RemoteViews) template presentation} when the condition
267          * is satisfied.
268          *
269          * @return this builder
270          *
271          * @throws IllegalArgumentException if {@code condition} is not a class provided
272          * by the Android System.
273          * @throws IllegalStateException if {@link #build()} was already called.
274          */
275         @NonNull
batchUpdate(@onNull Validator condition, @NonNull BatchUpdates updates)276         public Builder batchUpdate(@NonNull Validator condition, @NonNull BatchUpdates updates) {
277             throwIfDestroyed();
278             Preconditions.checkArgument((condition instanceof InternalValidator),
279                     "not provided by Android System: %s", condition);
280             Objects.requireNonNull(updates);
281             if (mUpdates == null) {
282                 mUpdates = new ArrayList<>();
283             }
284             mUpdates.add(new Pair<>((InternalValidator) condition, updates));
285             return this;
286         }
287 
288         /**
289          * Sets an action to be applied to the {@link RemoteViews presentation template} when the
290          * child view with the given {@code id} is clicked.
291          *
292          * <p>Typically used when the presentation uses a masked field (like {@code ****}) for
293          * sensitive fields like passwords or credit cards numbers, but offers a an icon that the
294          * user can tap to show the value for that field.
295          *
296          * <p>Example:
297          *
298          * <pre class="prettyprint">
299          * customDescriptionBuilder
300          *   .addChild(R.id.password_plain, new CharSequenceTransformation
301          *      .Builder(passwordId, Pattern.compile("^(.*)$"), "$1").build())
302          *   .addOnClickAction(R.id.showIcon, new VisibilitySetterAction
303          *     .Builder(R.id.hideIcon, View.VISIBLE)
304          *     .setVisibility(R.id.showIcon, View.GONE)
305          *     .setVisibility(R.id.password_plain, View.VISIBLE)
306          *     .setVisibility(R.id.password_masked, View.GONE)
307          *     .build())
308          *   .addOnClickAction(R.id.hideIcon, new VisibilitySetterAction
309          *     .Builder(R.id.showIcon, View.VISIBLE)
310          *     .setVisibility(R.id.hideIcon, View.GONE)
311          *     .setVisibility(R.id.password_masked, View.VISIBLE)
312          *     .setVisibility(R.id.password_plain, View.GONE)
313          *     .build());
314          * </pre>
315          *
316          * <p><b>Note:</b> Currently only one action can be applied to a child; if this method
317          * is called multiple times passing the same {@code id}, only the last call will be used.
318          *
319          * @param id resource id of the child view.
320          * @param action action to be performed. Must be an an implementation provided by the
321          * Android System.
322          *
323          * @return this builder
324          *
325          * @throws IllegalArgumentException if {@code action} is not a class provided
326          * by the Android System.
327          * @throws IllegalStateException if {@link #build()} was already called.
328          */
329         @NonNull
addOnClickAction(int id, @NonNull OnClickAction action)330         public Builder addOnClickAction(int id, @NonNull OnClickAction action) {
331             throwIfDestroyed();
332             Preconditions.checkArgument((action instanceof InternalOnClickAction),
333                     "not provided by Android System: %s", action);
334             if (mActions == null) {
335                 mActions = new SparseArray<InternalOnClickAction>();
336             }
337             mActions.put(id, (InternalOnClickAction) action);
338 
339             return this;
340         }
341 
342         /**
343          * Creates a new {@link CustomDescription} instance.
344          */
345         @NonNull
build()346         public CustomDescription build() {
347             throwIfDestroyed();
348             mDestroyed = true;
349             return new CustomDescription(this);
350         }
351 
throwIfDestroyed()352         private void throwIfDestroyed() {
353             if (mDestroyed) {
354                 throw new IllegalStateException("Already called #build()");
355             }
356         }
357     }
358 
359     /////////////////////////////////////
360     // Object "contract" methods. //
361     /////////////////////////////////////
362     @Override
toString()363     public String toString() {
364         if (!sDebug) return super.toString();
365 
366         return new StringBuilder("CustomDescription: [presentation=")
367                 .append(mPresentation)
368                 .append(", transformations=")
369                     .append(mTransformations == null ? "N/A" : mTransformations.size())
370                 .append(", updates=")
371                     .append(mUpdates == null ? "N/A" : mUpdates.size())
372                 .append(", actions=")
373                     .append(mActions == null ? "N/A" : mActions.size())
374                 .append("]").toString();
375     }
376 
377     /////////////////////////////////////
378     // Parcelable "contract" methods. //
379     /////////////////////////////////////
380     @Override
describeContents()381     public int describeContents() {
382         return 0;
383     }
384 
385     @Override
writeToParcel(Parcel dest, int flags)386     public void writeToParcel(Parcel dest, int flags) {
387         dest.writeParcelable(mPresentation, flags);
388         if (mPresentation == null) return;
389 
390         if (mTransformations == null) {
391             dest.writeIntArray(null);
392         } else {
393             final int size = mTransformations.size();
394             final int[] ids = new int[size];
395             final InternalTransformation[] values = new InternalTransformation[size];
396             for (int i = 0; i < size; i++) {
397                 final Pair<Integer, InternalTransformation> pair = mTransformations.get(i);
398                 ids[i] = pair.first;
399                 values[i] = pair.second;
400             }
401             dest.writeIntArray(ids);
402             dest.writeParcelableArray(values, flags);
403         }
404         if (mUpdates == null) {
405             dest.writeParcelableArray(null, flags);
406         } else {
407             final int size = mUpdates.size();
408             final InternalValidator[] conditions = new InternalValidator[size];
409             final BatchUpdates[] updates = new BatchUpdates[size];
410 
411             for (int i = 0; i < size; i++) {
412                 final Pair<InternalValidator, BatchUpdates> pair = mUpdates.get(i);
413                 conditions[i] = pair.first;
414                 updates[i] = pair.second;
415             }
416             dest.writeParcelableArray(conditions, flags);
417             dest.writeParcelableArray(updates, flags);
418         }
419         if (mActions == null) {
420             dest.writeIntArray(null);
421         } else {
422             final int size = mActions.size();
423             final int[] ids = new int[size];
424             final InternalOnClickAction[] values = new InternalOnClickAction[size];
425             for (int i = 0; i < size; i++) {
426                 ids[i] = mActions.keyAt(i);
427                 values[i] = mActions.valueAt(i);
428             }
429             dest.writeIntArray(ids);
430             dest.writeParcelableArray(values, flags);
431         }
432     }
433     public static final @android.annotation.NonNull Parcelable.Creator<CustomDescription> CREATOR =
434             new Parcelable.Creator<CustomDescription>() {
435         @Override
436         public CustomDescription createFromParcel(Parcel parcel) {
437             // Always go through the builder to ensure the data ingested by
438             // the system obeys the contract of the builder to avoid attacks
439             // using specially crafted parcels.
440             final RemoteViews parentPresentation = parcel.readParcelable(null, android.widget.RemoteViews.class);
441             if (parentPresentation == null) return null;
442 
443             final Builder builder = new Builder(parentPresentation);
444             final int[] transformationIds = parcel.createIntArray();
445             if (transformationIds != null) {
446                 final InternalTransformation[] values =
447                     parcel.readParcelableArray(null, InternalTransformation.class);
448                 final int size = transformationIds.length;
449                 for (int i = 0; i < size; i++) {
450                     builder.addChild(transformationIds[i], values[i]);
451                 }
452             }
453             final InternalValidator[] conditions =
454                     parcel.readParcelableArray(null, InternalValidator.class);
455             if (conditions != null) {
456                 final BatchUpdates[] updates = parcel.readParcelableArray(null, BatchUpdates.class);
457                 final int size = conditions.length;
458                 for (int i = 0; i < size; i++) {
459                     builder.batchUpdate(conditions[i], updates[i]);
460                 }
461             }
462             final int[] actionIds = parcel.createIntArray();
463             if (actionIds != null) {
464                 final InternalOnClickAction[] values =
465                     parcel.readParcelableArray(null, InternalOnClickAction.class);
466                 final int size = actionIds.length;
467                 for (int i = 0; i < size; i++) {
468                     builder.addOnClickAction(actionIds[i], values[i]);
469                 }
470             }
471             return builder.build();
472         }
473 
474         @Override
475         public CustomDescription[] newArray(int size) {
476             return new CustomDescription[size];
477         }
478     };
479 }
480