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