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