FillResponse.java revision 6ee1ed48b583971915759ce8f6e506168f4dfa78
1/* 2 * Copyright (C) 2016 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 */ 16package android.service.autofill; 17 18import static android.view.autofill.Helper.DEBUG; 19 20import android.annotation.NonNull; 21import android.annotation.Nullable; 22import android.content.IntentSender; 23import android.os.Bundle; 24import android.os.Parcel; 25import android.os.Parcelable; 26import android.view.autofill.AutoFillId; 27import android.view.autofill.AutoFillManager; 28import android.widget.RemoteViews; 29 30import java.util.ArrayList; 31 32/** 33 * Response for a {@link 34 * AutoFillService#onFillRequest(android.app.assist.AssistStructure, 35 * Bundle, android.os.CancellationSignal, FillCallback)}. 36 * 37 * <p>The response typically contains one or more {@link Dataset}s, each representing a set of 38 * fields that can be auto-filled together, and the Android system displays a dataset picker UI 39 * affordance that the user must use before the {@link android.app.Activity} is filled with 40 * the dataset. 41 * 42 * <p>For example, for a login page with username/password where the user only has one account in 43 * the response could be: 44 * 45 * <pre class="prettyprint"> 46 * new FillResponse.Builder() 47 * .add(new Dataset.Builder(createPresentation()) 48 * .setTextFieldValue(id1, "homer") 49 * .setTextFieldValue(id2, "D'OH!") 50 * .build()) 51 * .build(); 52 * </pre> 53 * 54 * <p>If the user had 2 accounts, each with its own user-provided names, the response could be: 55 * 56 * <pre class="prettyprint"> 57 * new FillResponse.Builder() 58 * .add(new Dataset.Builder(createFirstPresentation()) 59 * .setTextFieldValue(id1, "homer") 60 * .setTextFieldValue(id2, "D'OH!") 61 * .build()) 62 * .add(new Dataset.Builder(createSecondPresentation()) 63 * .setTextFieldValue(id1, "elbarto") 64 * .setTextFieldValue(id2, "cowabonga") 65 * .build()) 66 * .build(); 67 * </pre> 68 * 69 * <p>If the user does not have any data associated with this {@link android.app.Activity} but 70 * the service wants to offer the user the option to save the data that was entered, then the 71 * service could populate the response with a {@link SaveInfo} instead of {@link Dataset}s: 72 * 73 * <pre class="prettyprint"> 74 * new FillResponse.Builder() 75 * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_INFO_TYPE_CREDENTIALS) 76 * .addSavableFields(id1, id2)) 77 * .build(); 78 * </pre> 79 * 80 * <p>Similarly, there might be cases where the user data on the service is enough to populate some 81 * fields but not all, and the service would still be interested on saving the other fields. In this 82 * scenario, the service could populate the response with both {@link Dataset}s and 83 * {@link SaveInfo}: 84 * 85 * <pre class="prettyprint"> 86 * new FillResponse.Builder() 87 * .add(new Dataset.Builder(createPresentation()) 88 * .setTextFieldValue(id1, "Homer") // first name 89 * .setTextFieldValue(id2, "Simpson") // last name 90 * .setTextFieldValue(id3, "742 Evergreen Terrace") // street 91 * .setTextFieldValue(id4, "Springfield") // city 92 * .build()) 93 * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_INFO_TYPE_ADDRESS) 94 * .addSavableFields(id5, id6)) // state and zipcode 95 * .build(); 96 * 97 * </pre> 98 * 99 * <p>Notice that the ids that are part of a dataset (ids 1 to 4, in this example) are automatically 100 * added to the {@code savableIds} list. 101 * 102 * <p>If the service has multiple {@link Dataset}s for different sections of the activity, 103 * for example, a user section for which there are two datasets followed by an address 104 * section for which there are two datasets for each user user, then it should "partition" 105 * the activity in sections and populate the response with just a subset of the data that would 106 * fulfill the first section (the name in our example); then once the user fills the first 107 * section and taps a field from the next section (the address in our example), the Android 108 * system would issue another request for that section, and so on. Note that if the user 109 * chooses to populate the first section with a service provided dataset, the subsequent request 110 * would contain the populated values so you don't try to provide suggestions for the first 111 * section but ony for the second one based on the context of what was already filled. For 112 * example, the first response could be: 113 * 114 * <pre class="prettyprint"> 115 * new FillResponse.Builder() 116 * .add(new Dataset.Builder(createFirstPresentation()) 117 * .setTextFieldValue(id1, "Homer") 118 * .setTextFieldValue(id2, "Simpson") 119 * .build()) 120 * .add(new Dataset.Builder(createSecondPresentation()) 121 * .setTextFieldValue(id1, "Bart") 122 * .setTextFieldValue(id2, "Simpson") 123 * .build()) 124 * .build(); 125 * </pre> 126 * 127 * <p>Then after the user picks the second dataset and taps the street field to 128 * trigger another auto-fill request, the second response could be: 129 * 130 * <pre class="prettyprint"> 131 * new FillResponse.Builder() 132 * .add(new Dataset.Builder(createThirdPresentation()) 133 * .setTextFieldValue(id3, "742 Evergreen Terrace") 134 * .setTextFieldValue(id4, "Springfield") 135 * .build()) 136 * .add(new Dataset.Builder(createFourthPresentation()) 137 * .setTextFieldValue(id3, "Springfield Power Plant") 138 * .setTextFieldValue(id4, "Springfield") 139 * .build()) 140 * .build(); 141 * </pre> 142 * 143 * <p>The service could require user authentication at the {@link FillResponse} or the 144 * {@link Dataset} level, prior to auto-filling an activity - see 145 * {@link FillResponse.Builder#setAuthentication(IntentSender, RemoteViews)} and 146 * {@link Dataset.Builder#setAuthentication(IntentSender)}. 147 * 148 * <p>It is recommended that you encrypt only the sensitive data but leave the labels unencrypted 149 * which would allow you to provide a dataset presentation views with labels and if the user 150 * chooses one of them challenge the user to authenticate. For example, if the user has a 151 * home and a work address the Home and Work labels could be stored unencrypted as they don't 152 * have any sensitive data while the address data is in an encrypted storage. If the user 153 * chooses Home, then the platform will start your authentication flow. If you encrypt all 154 * data and require auth at the response level the user will have to interact with the fill 155 * UI to trigger a request for the datasets (as they don't see the presentation views for the 156 * possible options) which will start your auth flow and after successfully authenticating 157 * the user will be presented with the Home and Work options to pick one. Hence, you have 158 * flexibility how to implement your auth while storing labels non-encrypted and data 159 * encrypted provides a better user experience.</p> 160 */ 161public final class FillResponse implements Parcelable { 162 163 private final ArrayList<Dataset> mDatasets; 164 private final SaveInfo mSaveInfo; 165 private final Bundle mExtras; 166 private final RemoteViews mPresentation; 167 private final IntentSender mAuthentication; 168 169 private FillResponse(@NonNull Builder builder) { 170 mDatasets = builder.mDatasets; 171 172 // TODO(b/33197203, 35727295): this is how mSaveInfo will be set once we don't support 173 // FillResponse.addSavableIds() 174 mSaveInfo = builder.mSaveInfo; 175 if (mSaveInfo != null) { 176 mSaveInfo.addSavableIds(mDatasets); 177 if (mSaveInfo.getSavableIds() == null) { 178 throw new IllegalArgumentException( 179 "need to provide at least one savable id on SaveInfo"); 180 } 181 } 182 183 mExtras = builder.mExtras; 184 mPresentation = builder.mPresentation; 185 mAuthentication = builder.mAuthentication; 186 } 187 188 /** @hide */ 189 public @Nullable Bundle getExtras() { 190 return mExtras; 191 } 192 193 /** @hide */ 194 public @Nullable ArrayList<Dataset> getDatasets() { 195 return mDatasets; 196 } 197 198 /** @hide */ 199 public @Nullable SaveInfo getSaveInfo() { 200 return mSaveInfo; 201 } 202 203 /** @hide */ 204 public @Nullable RemoteViews getPresentation() { 205 return mPresentation; 206 } 207 208 /** @hide */ 209 public @Nullable IntentSender getAuthentication() { 210 return mAuthentication; 211 } 212 213 /** 214 * Builder for {@link FillResponse} objects. You must to provide at least 215 * one dataset or set an authentication intent with a presentation view. 216 */ 217 public static final class Builder { 218 private ArrayList<Dataset> mDatasets; 219 private SaveInfo mSaveInfo; 220 private Bundle mExtras; 221 private RemoteViews mPresentation; 222 private IntentSender mAuthentication; 223 private boolean mDestroyed; 224 225 /** 226 * Requires a fill response authentication before auto-filling the activity with 227 * any data set in this response. 228 * 229 * <p>This is typically useful when a user interaction is required to unlock their 230 * data vault if you encrypt the data set labels and data set data. It is recommended 231 * to encrypt only the sensitive data and not the data set labels which would allow 232 * auth on the data set level leading to a better user experience. Note that if you 233 * use sensitive data as a label, for example an email address, then it should also 234 * be encrypted. The provided {@link android.app.PendingIntent intent} must be an 235 * activity which implements your authentication flow. Also if you provide an auth 236 * intent you also need to specify the presentation view to be shown in the fill UI 237 * for the user to trigger your authentication flow.</p> 238 * 239 * <p>When a user triggers auto-fill, the system launches the provided intent 240 * whose extras will have the {@link AutoFillManager#EXTRA_ASSIST_STRUCTURE screen 241 * content}. Once you complete your authentication flow you should set the activity 242 * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated 243 * {@link FillResponse response} by setting it to the {@link 244 * AutoFillManager#EXTRA_AUTHENTICATION_RESULT} extra. 245 * For example, if you provided an empty {@link FillResponse resppnse} because the 246 * user's data was locked and marked that the response needs an authentication then 247 * in the response returned if authentication succeeds you need to provide all 248 * available data sets some of which may need to be further authenticated, for 249 * example a credit card whose CVV needs to be entered.</p> 250 * 251 * <p>If you provide an authentication intent you must also provide a presentation 252 * which is used to visualize visualize the response for triggering the authentication 253 * flow.</p> 254 * 255 * <p></><strong>Note:</strong> Do not make the provided pending intent 256 * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the 257 * platform needs to fill in the authentication arguments.</p> 258 * 259 * @param authentication Intent to an activity with your authentication flow. 260 * @param presentation The presentation to visualize the response. 261 * @return This builder. 262 * 263 * @see android.app.PendingIntent#getIntentSender() 264 */ 265 public @NonNull Builder setAuthentication(@Nullable IntentSender authentication, 266 @Nullable RemoteViews presentation) { 267 throwIfDestroyed(); 268 if (authentication == null ^ presentation == null) { 269 throw new IllegalArgumentException("authentication and presentation" 270 + " must be both non-null or null"); 271 } 272 mAuthentication = authentication; 273 mPresentation = presentation; 274 return this; 275 } 276 277 /** 278 * Adds a new {@link Dataset} to this response. 279 * 280 * @return This builder. 281 */ 282 public@NonNull Builder addDataset(@Nullable Dataset dataset) { 283 throwIfDestroyed(); 284 if (dataset == null) { 285 return this; 286 } 287 if (mDatasets == null) { 288 mDatasets = new ArrayList<>(); 289 } 290 if (!mDatasets.add(dataset)) { 291 return this; 292 } 293 return this; 294 } 295 296 /** 297 * Sets the {@link SaveInfo} associated with this response. 298 * 299 * <p>See {@link FillResponse} for more info. 300 * 301 * @return This builder. 302 */ 303 public @NonNull Builder setSaveInfo(@NonNull SaveInfo saveInfo) { 304 throwIfDestroyed(); 305 mSaveInfo = saveInfo; 306 return this; 307 } 308 309 /** 310 * Sets a {@link Bundle} that will be passed to subsequent APIs that 311 * manipulate this response. For example, they are passed to subsequent 312 * calls to {@link AutoFillService#onFillRequest( 313 * android.app.assist.AssistStructure, Bundle, android.os.CancellationSignal, 314 * FillCallback)} and {@link AutoFillService#onSaveRequest( 315 * android.app.assist.AssistStructure, Bundle, SaveCallback)}. 316 * 317 * @param extras The response extras. 318 * @return This builder. 319 */ 320 public Builder setExtras(Bundle extras) { 321 throwIfDestroyed(); 322 mExtras = extras; 323 return this; 324 } 325 326 /** 327 * Builds a new {@link FillResponse} instance. You must provide at least 328 * one dataset or some savable ids or an authentication with a presentation 329 * view. 330 * 331 * @return A built response. 332 */ 333 public FillResponse build() { 334 throwIfDestroyed(); 335 336 if (mAuthentication == null && mDatasets == null && mSaveInfo == null) { 337 throw new IllegalArgumentException("need to provide at least one DataSet or a " 338 + "SaveInfo or an authentication with a presentation"); 339 } 340 mDestroyed = true; 341 return new FillResponse(this); 342 } 343 344 private void throwIfDestroyed() { 345 if (mDestroyed) { 346 throw new IllegalStateException("Already called #build()"); 347 } 348 } 349 } 350 351 ///////////////////////////////////// 352 // Object "contract" methods. // 353 ///////////////////////////////////// 354 @Override 355 public String toString() { 356 if (!DEBUG) return super.toString(); 357 358 return new StringBuilder( 359 "FillResponse: [datasets=").append(mDatasets) 360 .append(", saveInfo=").append(mSaveInfo) 361 .append(", hasExtras=").append(mExtras != null) 362 .append(", hasPresentation=").append(mPresentation != null) 363 .append(", hasAuthentication=").append(mAuthentication != null) 364 .toString(); 365 } 366 367 ///////////////////////////////////// 368 // Parcelable "contract" methods. // 369 ///////////////////////////////////// 370 371 @Override 372 public int describeContents() { 373 return 0; 374 } 375 376 @Override 377 public void writeToParcel(Parcel parcel, int flags) { 378 parcel.writeTypedArrayList(mDatasets, flags); 379 parcel.writeParcelable(mSaveInfo, flags); 380 parcel.writeParcelable(mExtras, flags); 381 parcel.writeParcelable(mAuthentication, flags); 382 parcel.writeParcelable(mPresentation, flags); 383 } 384 385 public static final Parcelable.Creator<FillResponse> CREATOR = 386 new Parcelable.Creator<FillResponse>() { 387 @Override 388 public FillResponse createFromParcel(Parcel parcel) { 389 // Always go through the builder to ensure the data ingested by 390 // the system obeys the contract of the builder to avoid attacks 391 // using specially crafted parcels. 392 final Builder builder = new Builder(); 393 final ArrayList<Dataset> datasets = parcel.readTypedArrayList(null); 394 final int datasetCount = (datasets != null) ? datasets.size() : 0; 395 for (int i = 0; i < datasetCount; i++) { 396 builder.addDataset(datasets.get(i)); 397 } 398 builder.setSaveInfo(parcel.readParcelable(null)); 399 builder.setExtras(parcel.readParcelable(null)); 400 builder.setAuthentication(parcel.readParcelable(null), 401 parcel.readParcelable(null)); 402 return builder.build(); 403 } 404 405 @Override 406 public FillResponse[] newArray(int size) { 407 return new FillResponse[size]; 408 } 409 }; 410} 411