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