SaveInfo.java revision 0962262f42e3636270689100e1bb90b8f3cf6d77
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 /** 134 * Type used when the {@link FillResponse} represents just an username, without a password. 135 */ 136 public static final int SAVE_DATA_TYPE_USERNAME = 4; 137 138 /** 139 * Type used when the {@link FillResponse} represents just an email address, without a password. 140 */ 141 public static final int SAVE_DATA_TYPE_EMAIL_ADDRESS = 5; 142 143 private final @SaveDataType int mType; 144 private final CharSequence mNegativeActionTitle; 145 private final IntentSender mNegativeActionListener; 146 private final AutofillId[] mRequiredIds; 147 private final AutofillId[] mOptionalIds; 148 private final CharSequence mDescription; 149 150 /** @hide */ 151 @IntDef({ 152 SAVE_DATA_TYPE_GENERIC, 153 SAVE_DATA_TYPE_PASSWORD, 154 SAVE_DATA_TYPE_ADDRESS, 155 SAVE_DATA_TYPE_CREDIT_CARD 156 }) 157 @Retention(RetentionPolicy.SOURCE) 158 public @interface SaveDataType { 159 } 160 161 private SaveInfo(Builder builder) { 162 mType = builder.mType; 163 mNegativeActionTitle = builder.mNegativeActionTitle; 164 mNegativeActionListener = builder.mNegativeActionListener; 165 mRequiredIds = builder.mRequiredIds; 166 mOptionalIds = builder.mOptionalIds; 167 mDescription = builder.mDescription; 168 } 169 170 /** @hide */ 171 public @Nullable CharSequence getNegativeActionTitle() { 172 return mNegativeActionTitle; 173 } 174 175 /** @hide */ 176 public @Nullable IntentSender getNegativeActionListener() { 177 return mNegativeActionListener; 178 } 179 180 /** @hide */ 181 public AutofillId[] getRequiredIds() { 182 return mRequiredIds; 183 } 184 185 /** @hide */ 186 public @Nullable AutofillId[] getOptionalIds() { 187 return mOptionalIds; 188 } 189 190 /** @hide */ 191 public int getType() { 192 return mType; 193 } 194 195 /** @hide */ 196 public CharSequence getDescription() { 197 return mDescription; 198 } 199 200 /** 201 * A builder for {@link SaveInfo} objects. 202 */ 203 public static final class Builder { 204 205 private final @SaveDataType int mType; 206 private CharSequence mNegativeActionTitle; 207 private IntentSender mNegativeActionListener; 208 // TODO(b/33197203): make mRequiredIds final once addSavableIds() is gone 209 private AutofillId[] mRequiredIds; 210 private AutofillId[] mOptionalIds; 211 private CharSequence mDescription; 212 private boolean mDestroyed; 213 214 /** 215 * Creates a new builder. 216 * 217 * @param type the type of information the associated {@link FillResponse} represents. Must 218 * be {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD}, 219 * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, or {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD}; 220 * otherwise it will assume {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}. 221 * @param requiredIds ids of all required views that will trigger a save request. 222 * 223 * <p>See {@link SaveInfo} for more info. 224 * 225 * @throws IllegalArgumentException if {@code requiredIds} is {@code null} or empty. 226 */ 227 public Builder(@SaveDataType int type, @NonNull AutofillId[] requiredIds) { 228 if (false) {// TODO(b/33197203): re-move when clients use it 229 Preconditions.checkArgument(requiredIds != null && requiredIds.length > 0, 230 "must have at least one required id: " + Arrays.toString(requiredIds)); 231 } 232 switch (type) { 233 case SAVE_DATA_TYPE_PASSWORD: 234 case SAVE_DATA_TYPE_ADDRESS: 235 case SAVE_DATA_TYPE_CREDIT_CARD: 236 case SAVE_DATA_TYPE_USERNAME: 237 case SAVE_DATA_TYPE_EMAIL_ADDRESS: 238 mType = type; 239 break; 240 default: 241 mType = SAVE_DATA_TYPE_GENERIC; 242 } 243 mRequiredIds = requiredIds; 244 } 245 246 /** 247 * @hide 248 * @deprecated 249 * // TODO(b/33197203): make sure is removed when clients migrated 250 */ 251 @Deprecated 252 public Builder(@SaveDataType int type) { 253 this(type, null); 254 } 255 256 /** 257 * @hide 258 * @deprecated 259 * // TODO(b/33197203): make sure is removed when clients migrated 260 */ 261 @Deprecated 262 public @NonNull Builder addSavableIds(@Nullable AutofillId... ids) { 263 throwIfDestroyed(); 264 mRequiredIds = ids; 265 return this; 266 } 267 268 /** 269 * Sets the ids of additional, optional views the service would be interested to save. 270 * 271 * <p>See {@link SaveInfo} for more info. 272 * 273 * @param ids The ids of the optional views. 274 * @return This builder. 275 */ 276 public @NonNull Builder setOptionalIds(@Nullable AutofillId[] ids) { 277 throwIfDestroyed(); 278 if (ids != null && ids.length != 0) { 279 mOptionalIds = ids; 280 } 281 return this; 282 } 283 284 285 /** 286 * @hide 287 */ 288 // TODO(b/33197203): temporary fix to runtime crash 289 public @NonNull Builder addSavableIds(@Nullable AutoFillId... ids) { 290 throwIfDestroyed(); 291 292 if (ids == null || ids.length == 0) { 293 return this; 294 } 295 if (mRequiredIds == null) { 296 mRequiredIds = new AutofillId[ids.length]; 297 } 298 for (int i = 0; i < ids.length; i++) { 299 mRequiredIds[i] = ids[i].getDaRealId(); 300 } 301 return this; 302 } 303 304 /** 305 * Sets an optional description to be shown in the UI when the user is asked to save. 306 * 307 * <p>Typically, it describes how the data will be stored by the service, so it can help 308 * users to decide whether they can trust the service to save their data. 309 * 310 * @param description a succint description. 311 * @return This Builder. 312 */ 313 public @NonNull Builder setDescription(@Nullable CharSequence description) { 314 throwIfDestroyed(); 315 mDescription = description; 316 return this; 317 } 318 319 /** 320 * Sets the title and listener for the negative save action. 321 * 322 * <p>This allows a fill-provider to customize the text and be 323 * notified when the user selects the negative action in the save 324 * UI. Note that selecting the negative action regardless of its text 325 * and listener being customized would dismiss the save UI and if a 326 * custom listener intent is provided then this intent will be 327 * started.</p> 328 * 329 * <p>This customization could be useful for providing additional 330 * semantics to the negative action. For example, a fill-provider 331 * can use this mechanism to add a "Disable" function or a "More info" 332 * function, etc. Note that the save action is exclusively controlled 333 * by the platform to ensure user consent is collected to release 334 * data from the filled app to the fill-provider.</p> 335 * 336 * @param title The action title. 337 * @param listener The action listener. 338 * @return This builder. 339 * 340 * @throws IllegalArgumentException If the title and the listener 341 * are not both either null or non-null. 342 */ 343 public @NonNull Builder setNegativeAction(@Nullable CharSequence title, 344 @Nullable IntentSender listener) { 345 throwIfDestroyed(); 346 if (title == null ^ listener == null) { 347 throw new IllegalArgumentException("title and listener" 348 + " must be both non-null or null"); 349 } 350 mNegativeActionTitle = title; 351 mNegativeActionListener = listener; 352 return this; 353 } 354 355 /** 356 * Builds a new {@link SaveInfo} instance. 357 */ 358 public SaveInfo build() { 359 throwIfDestroyed(); 360 mDestroyed = true; 361 return new SaveInfo(this); 362 } 363 364 private void throwIfDestroyed() { 365 if (mDestroyed) { 366 throw new IllegalStateException("Already called #build()"); 367 } 368 } 369 370 } 371 372 ///////////////////////////////////// 373 // Object "contract" methods. // 374 ///////////////////////////////////// 375 @Override 376 public String toString() { 377 if (!DEBUG) return super.toString(); 378 379 return new StringBuilder("SaveInfo: [type=").append(mType) 380 .append(", requiredIds=").append(Arrays.toString(mRequiredIds)) 381 .append(", optionalIds=").append(Arrays.toString(mOptionalIds)) 382 .append(", description=").append(mDescription) 383 .append("]").toString(); 384 } 385 386 ///////////////////////////////////// 387 // Parcelable "contract" methods. // 388 ///////////////////////////////////// 389 390 @Override 391 public int describeContents() { 392 return 0; 393 } 394 395 @Override 396 public void writeToParcel(Parcel parcel, int flags) { 397 parcel.writeInt(mType); 398 parcel.writeParcelableArray(mRequiredIds, flags); 399 parcel.writeCharSequence(mNegativeActionTitle); 400 parcel.writeParcelable(mNegativeActionListener, flags); 401 parcel.writeParcelableArray(mOptionalIds, flags); 402 parcel.writeCharSequence(mDescription); 403 } 404 405 public static final Parcelable.Creator<SaveInfo> CREATOR = new Parcelable.Creator<SaveInfo>() { 406 @Override 407 public SaveInfo createFromParcel(Parcel parcel) { 408 // Always go through the builder to ensure the data ingested by 409 // the system obeys the contract of the builder to avoid attacks 410 // using specially crafted parcels. 411 final Builder builder = new Builder(parcel.readInt(), 412 parcel.readParcelableArray(null, AutofillId.class)); 413 builder.setNegativeAction(parcel.readCharSequence(), parcel.readParcelable(null)); 414 builder.setOptionalIds(parcel.readParcelableArray(null, AutofillId.class)); 415 builder.setDescription(parcel.readCharSequence()); 416 return builder.build(); 417 } 418 419 @Override 420 public SaveInfo[] newArray(int size) { 421 return new SaveInfo[size]; 422 } 423 }; 424} 425