SaveInfo.java revision 2ac463e3f5c3b757ecbc7e30e0fc1e8e0d878272
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.DEBUG; 20 21import android.annotation.IntDef; 22import android.annotation.NonNull; 23import android.annotation.Nullable; 24import android.content.IntentSender; 25import android.os.Bundle; 26import android.os.Parcel; 27import android.os.Parcelable; 28import android.view.autofill.AutoFillId; 29import android.view.autofill.AutofillId; 30import android.view.autofill.AutofillManager; 31import android.view.autofill.AutofillValue; 32 33import com.android.internal.util.Preconditions; 34 35import java.lang.annotation.Retention; 36import java.lang.annotation.RetentionPolicy; 37import java.util.Arrays; 38 39/** 40 * Information used to indicate that an {@link AutofillService} is interested on saving the 41 * user-inputed data for future use, through a 42 * {@link AutofillService#onSaveRequest(android.app.assist.AssistStructure, Bundle, SaveCallback)} 43 * call. 44 * 45 * <p>A {@link SaveInfo} is always associated with a {@link FillResponse}, and it contains at least 46 * two pieces of information: 47 * 48 * <ol> 49 * <li>The type of user data that would be saved (like passoword or credit card info). 50 * <li>The minimum set of views (represented by their {@link AutofillId}) that need to be changed 51 * to trigger a save request. 52 * </ol> 53 * 54 * Typically, the {@link SaveInfo} contains the same {@code id}s as the {@link Dataset}: 55 * 56 * <pre class="prettyprint"> 57 * new FillResponse.Builder() 58 * .add(new Dataset.Builder(createPresentation()) 59 * .setValue(id1, AutofillValue.forText("homer")) 60 * .setValue(id2, AutofillValue.forText("D'OH!")) 61 * .build()) 62 * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_INFO_TYPE_PASSWORD, new int[] {id1, id2}) 63 * .build()) 64 * .build(); 65 * </pre> 66 * 67 * There might be cases where the {@link AutofillService} knows how to fill the 68 * {@link android.app.Activity}, but the user has no data for it. In that case, the 69 * {@link FillResponse} should contain just the {@link SaveInfo}, but no {@link Dataset}s: 70 * 71 * <pre class="prettyprint"> 72 * new FillResponse.Builder() 73 * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_INFO_TYPE_PASSWORD, new int[] {id1, id2}) 74 * .build()) 75 * .build(); 76 * </pre> 77 * 78 * <p>There might be cases where the user data in the {@link AutofillService} is enough 79 * to populate some fields but not all, and the service would still be interested on saving the 80 * other fields. In this scenario, the service could set the 81 * {@link SaveInfo.Builder#setOptionalIds(AutofillId[])} as well: 82 * 83 * <pre class="prettyprint"> 84 * new FillResponse.Builder() 85 * .add(new Dataset.Builder(createPresentation()) 86 * .setValue(id1, AutofillValue.forText("742 Evergreen Terrace")) // street 87 * .setValue(id2, AutofillValue.forText("Springfield")) // city 88 * .build()) 89 * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_INFO_TYPE_ADDRESS, new int[] {id1, id2}) 90 * .setOptionalIds(new int[] {id3, id4}) // state and zipcode 91 * .build()) 92 * .build(); 93 * </pre> 94 * 95 * The 96 * {@link AutofillService#onSaveRequest(android.app.assist.AssistStructure, Bundle, SaveCallback)} 97 * is triggered after a call to {@link AutofillManager#commit()}, but only when all conditions 98 * below are met: 99 * 100 * <ol> 101 * <li>The {@link SaveInfo} associated with the {@link FillResponse} is not {@code null}. 102 * <li>The {@link AutofillValue} of all required views (as set by the {@code requiredIds} passed 103 * to {@link SaveInfo.Builder} constructor are not empty. 104 * <li>The {@link AutofillValue} of at least one view (be it required or optional) has changed 105 * (i.e., it's not the same value passed in a {@link Dataset}). 106 * <li>The user explicitly tapped the affordance asking to save data for autofill. 107 * </ol> 108 */ 109public final class SaveInfo implements Parcelable { 110 111 /** 112 * Type used on when the service can save the contents of an activity, but cannot describe what 113 * the content is for. 114 */ 115 public static final int SAVE_DATA_TYPE_GENERIC = 0; 116 117 /** 118 * Type used when the {@link FillResponse} represents user credentials that have a password. 119 */ 120 public static final int SAVE_DATA_TYPE_PASSWORD = 1; 121 122 /** 123 * Type used on when the {@link FillResponse} represents a physical address (such as street, 124 * city, state, etc). 125 */ 126 public static final int SAVE_DATA_TYPE_ADDRESS = 2; 127 128 /** 129 * Type used when the {@link FillResponse} represents a credit card. 130 */ 131 public static final int SAVE_DATA_TYPE_CREDIT_CARD = 3; 132 133 private final @SaveDataType int mType; 134 private final CharSequence mNegativeActionTitle; 135 private final IntentSender mNegativeActionListener; 136 private final AutofillId[] mRequiredIds; 137 private final AutofillId[] mOptionalIds; 138 private final CharSequence mDescription; 139 140 /** @hide */ 141 @IntDef({ 142 SAVE_DATA_TYPE_GENERIC, 143 SAVE_DATA_TYPE_PASSWORD, 144 SAVE_DATA_TYPE_ADDRESS, 145 SAVE_DATA_TYPE_CREDIT_CARD 146 }) 147 @Retention(RetentionPolicy.SOURCE) 148 public @interface SaveDataType { 149 } 150 151 private SaveInfo(Builder builder) { 152 mType = builder.mType; 153 mNegativeActionTitle = builder.mNegativeActionTitle; 154 mNegativeActionListener = builder.mNegativeActionListener; 155 mRequiredIds = builder.mRequiredIds; 156 mOptionalIds = builder.mOptionalIds; 157 mDescription = builder.mDescription; 158 } 159 160 /** @hide */ 161 public @Nullable CharSequence getNegativeActionTitle() { 162 return mNegativeActionTitle; 163 } 164 165 /** @hide */ 166 public @Nullable IntentSender getNegativeActionListener() { 167 return mNegativeActionListener; 168 } 169 170 /** @hide */ 171 public AutofillId[] getRequiredIds() { 172 return mRequiredIds; 173 } 174 175 /** @hide */ 176 public @Nullable AutofillId[] getOptionalIds() { 177 return mOptionalIds; 178 } 179 180 /** @hide */ 181 public int getType() { 182 return mType; 183 } 184 185 /** @hide */ 186 public CharSequence getDescription() { 187 return mDescription; 188 } 189 190 /** 191 * A builder for {@link SaveInfo} objects. 192 */ 193 public static final class Builder { 194 195 private final @SaveDataType int mType; 196 private CharSequence mNegativeActionTitle; 197 private IntentSender mNegativeActionListener; 198 // TODO(b/33197203): make mRequiredIds final once addSavableIds() is gone 199 private AutofillId[] mRequiredIds; 200 private AutofillId[] mOptionalIds; 201 private CharSequence mDescription; 202 private boolean mDestroyed; 203 204 /** 205 * Creates a new builder. 206 * 207 * @param type the type of information the associated {@link FillResponse} represents. Must 208 * be {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD}, 209 * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, or {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD}; 210 * otherwise it will assume {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}. 211 * @param requiredIds ids of all required views that will trigger a save request. 212 * 213 * <p>See {@link SaveInfo} for more info. 214 * 215 * @throws IllegalArgumentException if {@code requiredIds} is {@code null} or empty. 216 */ 217 public Builder(@SaveDataType int type, @NonNull AutofillId[] requiredIds) { 218 Preconditions.checkArgument(requiredIds != null && requiredIds.length > 0, 219 "must have at least on required id: " + Arrays.toString(requiredIds)); 220 switch (type) { 221 case SAVE_DATA_TYPE_PASSWORD: 222 case SAVE_DATA_TYPE_ADDRESS: 223 case SAVE_DATA_TYPE_CREDIT_CARD: 224 mType = type; 225 break; 226 default: 227 mType = SAVE_DATA_TYPE_GENERIC; 228 } 229 mRequiredIds = requiredIds; 230 } 231 232 /** 233 * @hide 234 * @deprecated 235 * // TODO(b/33197203): make sure is removed when clients migrated 236 */ 237 @Deprecated 238 public Builder(@SaveDataType int type) { 239 this(type, null); 240 } 241 242 /** 243 * @hide 244 * @deprecated 245 * // TODO(b/33197203): make sure is removed when clients migrated 246 */ 247 @Deprecated 248 public @NonNull Builder addSavableIds(@Nullable AutofillId... ids) { 249 throwIfDestroyed(); 250 mRequiredIds = ids; 251 return this; 252 } 253 254 /** 255 * Sets the ids of additional, optional views the service would be interested to save. 256 * 257 * <p>See {@link SaveInfo} for more info. 258 * 259 * @param ids The ids of the optional views. 260 * @return This builder. 261 */ 262 public @NonNull Builder setOptionalIds(@Nullable AutofillId[] ids) { 263 throwIfDestroyed(); 264 if (ids != null && ids.length != 0) { 265 mOptionalIds = ids; 266 } 267 return this; 268 } 269 270 271 /** 272 * @hide 273 */ 274 // TODO(b/33197203): temporary fix to runtime crash 275 public @NonNull Builder addSavableIds(@Nullable AutoFillId... ids) { 276 throwIfDestroyed(); 277 278 if (ids == null || ids.length == 0) { 279 return this; 280 } 281 if (mRequiredIds == null) { 282 mRequiredIds = new AutofillId[ids.length]; 283 } 284 for (int i = 0; i < ids.length; i++) { 285 mRequiredIds[i] = ids[i].getDaRealId(); 286 } 287 return this; 288 } 289 290 /** 291 * Sets an optional description to be shown in the UI when the user is asked to save. 292 * 293 * <p>Typically, it describes how the data will be stored by the service, so it can help 294 * users to decide whether they can trust the service to save their data. 295 * 296 * @param description a succint description. 297 * @return This Builder. 298 */ 299 public @NonNull Builder setDescription(@Nullable CharSequence description) { 300 throwIfDestroyed(); 301 mDescription = description; 302 return this; 303 } 304 305 /** 306 * Sets the title and listener for the negative save action. 307 * 308 * <p>This allows a fill-provider to customize the text and be 309 * notified when the user selects the negative action in the save 310 * UI. Note that selecting the negative action regardless of its text 311 * and listener being customized would dismiss the save UI and if a 312 * custom listener intent is provided then this intent will be 313 * started.</p> 314 * 315 * <p>This customization could be useful for providing additional 316 * semantics to the negative action. For example, a fill-provider 317 * can use this mechanism to add a "Disable" function or a "More info" 318 * function, etc. Note that the save action is exclusively controlled 319 * by the platform to ensure user consent is collected to release 320 * data from the filled app to the fill-provider.</p> 321 * 322 * @param title The action title. 323 * @param listener The action listener. 324 * @return This builder. 325 * 326 * @throws IllegalArgumentException If the title and the listener 327 * are not both either null or non-null. 328 */ 329 public @NonNull Builder setNegativeAction(@Nullable CharSequence title, 330 @Nullable IntentSender listener) { 331 throwIfDestroyed(); 332 if (title == null ^ listener == null) { 333 throw new IllegalArgumentException("title and listener" 334 + " must be both non-null or null"); 335 } 336 mNegativeActionTitle = title; 337 mNegativeActionListener = listener; 338 return this; 339 } 340 341 /** 342 * Builds a new {@link SaveInfo} instance. 343 */ 344 public SaveInfo build() { 345 throwIfDestroyed(); 346 mDestroyed = true; 347 return new SaveInfo(this); 348 } 349 350 private void throwIfDestroyed() { 351 if (mDestroyed) { 352 throw new IllegalStateException("Already called #build()"); 353 } 354 } 355 356 } 357 358 ///////////////////////////////////// 359 // Object "contract" methods. // 360 ///////////////////////////////////// 361 @Override 362 public String toString() { 363 if (!DEBUG) return super.toString(); 364 365 return new StringBuilder("SaveInfo: [type=").append(mType) 366 .append(", requiredIds=").append(Arrays.toString(mRequiredIds)) 367 .append(", optionalIds=").append(Arrays.toString(mOptionalIds)) 368 .append(", description=").append(mDescription) 369 .append("]").toString(); 370 } 371 372 ///////////////////////////////////// 373 // Parcelable "contract" methods. // 374 ///////////////////////////////////// 375 376 @Override 377 public int describeContents() { 378 return 0; 379 } 380 381 @Override 382 public void writeToParcel(Parcel parcel, int flags) { 383 parcel.writeInt(mType); 384 parcel.writeParcelableArray(mRequiredIds, flags); 385 parcel.writeCharSequence(mNegativeActionTitle); 386 parcel.writeParcelable(mNegativeActionListener, flags); 387 parcel.writeParcelableArray(mOptionalIds, flags); 388 parcel.writeCharSequence(mDescription); 389 } 390 391 public static final Parcelable.Creator<SaveInfo> CREATOR = new Parcelable.Creator<SaveInfo>() { 392 @Override 393 public SaveInfo createFromParcel(Parcel parcel) { 394 // Always go through the builder to ensure the data ingested by 395 // the system obeys the contract of the builder to avoid attacks 396 // using specially crafted parcels. 397 final Builder builder = new Builder(parcel.readInt(), 398 parcel.readParcelableArray(null, AutofillId.class)); 399 builder.setNegativeAction(parcel.readCharSequence(), parcel.readParcelable(null)); 400 builder.setOptionalIds(parcel.readParcelableArray(null, AutofillId.class)); 401 builder.setDescription(parcel.readCharSequence()); 402 return builder.build(); 403 } 404 405 @Override 406 public SaveInfo[] newArray(int size) { 407 return new SaveInfo[size]; 408 } 409 }; 410} 411