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