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 17 package android.service.autofill; 18 19 import static android.view.autofill.Helper.sDebug; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.IntentSender; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.view.autofill.AutofillId; 27 import android.view.autofill.AutofillValue; 28 import android.widget.RemoteViews; 29 30 import com.android.internal.util.Preconditions; 31 32 import java.util.ArrayList; 33 import java.util.regex.Pattern; 34 35 /** 36 * <p>A <code>Dataset</code> object represents a group of fields (key / value pairs) used 37 * to autofill parts of a screen. 38 * 39 * <p>For more information about the role of datasets in the autofill workflow, read 40 * <a href="/guide/topics/text/autofill-services">Build autofill services</a> and the 41 * <code><a href="/reference/android/service/autofill/AutofillService">AutofillService</a></code> 42 * documentation. 43 * 44 * <a name="BasicUsage"></a> 45 * <h3>Basic usage</h3> 46 * 47 * <p>In its simplest form, a dataset contains one or more fields (comprised of 48 * an {@link AutofillId id}, a {@link AutofillValue value}, and an optional filter 49 * {@link Pattern regex}); and one or more {@link RemoteViews presentations} for these fields 50 * (each field could have its own {@link RemoteViews presentation}, or use the default 51 * {@link RemoteViews presentation} associated with the whole dataset). 52 * 53 * <p>When an autofill service returns datasets in a {@link FillResponse} 54 * and the screen input is focused in a view that is present in at least one of these datasets, 55 * the Android System displays a UI containing the {@link RemoteViews presentation} of 56 * all datasets pairs that have that view's {@link AutofillId}. Then, when the user selects a 57 * dataset from the UI, all views in that dataset are autofilled. 58 * 59 * <a name="Authentication"></a> 60 * <h3>Dataset authentication</h3> 61 * 62 * <p>In a more sophisticated form, the dataset values can be protected until the user authenticates 63 * the dataset—in that case, when a dataset is selected by the user, the Android System 64 * launches an intent set by the service to "unlock" the dataset. 65 * 66 * <p>For example, when a data set contains credit card information (such as number, 67 * expiration date, and verification code), you could provide a dataset presentation saying 68 * "Tap to authenticate". Then when the user taps that option, you would launch an activity asking 69 * the user to enter the credit card code, and if the user enters a valid code, you could then 70 * "unlock" the dataset. 71 * 72 * <p>You can also use authenticated datasets to offer an interactive UI for the user. For example, 73 * if the activity being autofilled is an account creation screen, you could use an authenticated 74 * dataset to automatically generate a random password for the user. 75 * 76 * <p>See {@link Dataset.Builder#setAuthentication(IntentSender)} for more details about the dataset 77 * authentication mechanism. 78 * 79 * <a name="Filtering"></a> 80 * <h3>Filtering</h3> 81 * <p>The autofill UI automatically changes which values are shown based on value of the view 82 * anchoring it, following the rules below: 83 * <ol> 84 * <li>If the view's {@link android.view.View#getAutofillValue() autofill value} is not 85 * {@link AutofillValue#isText() text} or is empty, all datasets are shown. 86 * <li>Datasets that have a filter regex (set through 87 * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern)} or 88 * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}) and whose 89 * regex matches the view's text value converted to lower case are shown. 90 * <li>Datasets that do not require authentication, have a field value that is 91 * {@link AutofillValue#isText() text} and whose {@link AutofillValue#getTextValue() value} starts 92 * with the lower case value of the view's text are shown. 93 * <li>All other datasets are hidden. 94 * </ol> 95 * 96 */ 97 public final class Dataset implements Parcelable { 98 99 private final ArrayList<AutofillId> mFieldIds; 100 private final ArrayList<AutofillValue> mFieldValues; 101 private final ArrayList<RemoteViews> mFieldPresentations; 102 private final ArrayList<DatasetFieldFilter> mFieldFilters; 103 private final RemoteViews mPresentation; 104 private final IntentSender mAuthentication; 105 @Nullable String mId; 106 Dataset(Builder builder)107 private Dataset(Builder builder) { 108 mFieldIds = builder.mFieldIds; 109 mFieldValues = builder.mFieldValues; 110 mFieldPresentations = builder.mFieldPresentations; 111 mFieldFilters = builder.mFieldFilters; 112 mPresentation = builder.mPresentation; 113 mAuthentication = builder.mAuthentication; 114 mId = builder.mId; 115 } 116 117 /** @hide */ getFieldIds()118 public @Nullable ArrayList<AutofillId> getFieldIds() { 119 return mFieldIds; 120 } 121 122 /** @hide */ getFieldValues()123 public @Nullable ArrayList<AutofillValue> getFieldValues() { 124 return mFieldValues; 125 } 126 127 /** @hide */ getFieldPresentation(int index)128 public RemoteViews getFieldPresentation(int index) { 129 final RemoteViews customPresentation = mFieldPresentations.get(index); 130 return customPresentation != null ? customPresentation : mPresentation; 131 } 132 133 /** @hide */ 134 @Nullable getFilter(int index)135 public DatasetFieldFilter getFilter(int index) { 136 return mFieldFilters.get(index); 137 } 138 139 /** @hide */ getAuthentication()140 public @Nullable IntentSender getAuthentication() { 141 return mAuthentication; 142 } 143 144 /** @hide */ isEmpty()145 public boolean isEmpty() { 146 return mFieldIds == null || mFieldIds.isEmpty(); 147 } 148 149 @Override toString()150 public String toString() { 151 if (!sDebug) return super.toString(); 152 153 final StringBuilder builder = new StringBuilder("Dataset["); 154 if (mId == null) { 155 builder.append("noId"); 156 } else { 157 // Cannot disclose id because it could contain PII. 158 builder.append("id=").append(mId.length()).append("_chars"); 159 } 160 if (mFieldIds != null) { 161 builder.append(", fieldIds=").append(mFieldIds); 162 } 163 if (mFieldValues != null) { 164 builder.append(", fieldValues=").append(mFieldValues); 165 } 166 if (mFieldPresentations != null) { 167 builder.append(", fieldPresentations=").append(mFieldPresentations.size()); 168 169 } 170 if (mFieldFilters != null) { 171 builder.append(", fieldFilters=").append(mFieldFilters.size()); 172 } 173 if (mPresentation != null) { 174 builder.append(", hasPresentation"); 175 } 176 if (mAuthentication != null) { 177 builder.append(", hasAuthentication"); 178 } 179 return builder.append(']').toString(); 180 } 181 182 /** 183 * Gets the id of this dataset. 184 * 185 * @return The id of this dataset or {@code null} if not set 186 * 187 * @hide 188 */ getId()189 public String getId() { 190 return mId; 191 } 192 193 /** 194 * A builder for {@link Dataset} objects. You must provide at least 195 * one value for a field or set an authentication intent. 196 */ 197 public static final class Builder { 198 private ArrayList<AutofillId> mFieldIds; 199 private ArrayList<AutofillValue> mFieldValues; 200 private ArrayList<RemoteViews> mFieldPresentations; 201 private ArrayList<DatasetFieldFilter> mFieldFilters; 202 private RemoteViews mPresentation; 203 private IntentSender mAuthentication; 204 private boolean mDestroyed; 205 @Nullable private String mId; 206 207 /** 208 * Creates a new builder. 209 * 210 * @param presentation The presentation used to visualize this dataset. 211 */ Builder(@onNull RemoteViews presentation)212 public Builder(@NonNull RemoteViews presentation) { 213 Preconditions.checkNotNull(presentation, "presentation must be non-null"); 214 mPresentation = presentation; 215 } 216 217 /** 218 * Creates a new builder for a dataset where each field will be visualized independently. 219 * 220 * <p>When using this constructor, fields must be set through 221 * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} or 222 * {@link #setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}. 223 */ Builder()224 public Builder() { 225 } 226 227 /** 228 * Triggers a custom UI before before autofilling the screen with the contents of this 229 * dataset. 230 * 231 * <p><b>Note:</b> Although the name of this method suggests that it should be used just for 232 * authentication flow, it can be used for other advanced flows; see {@link AutofillService} 233 * for examples. 234 * 235 * <p>This method is called when you need to provide an authentication 236 * UI for the data set. For example, when a data set contains credit card information 237 * (such as number, expiration date, and verification code), you can display UI 238 * asking for the verification code before filing in the data. Even if the 239 * data set is completely populated the system will launch the specified authentication 240 * intent and will need your approval to fill it in. Since the data set is "locked" 241 * until the user authenticates it, typically this data set name is masked 242 * (for example, "VISA....1234"). Typically you would want to store the data set 243 * labels non-encrypted and the actual sensitive data encrypted and not in memory. 244 * This allows showing the labels in the UI while involving the user if one of 245 * the items with these labels is chosen. Note that if you use sensitive data as 246 * a label, for example an email address, then it should also be encrypted.</p> 247 * 248 * <p>When a user triggers autofill, the system launches the provided intent 249 * whose extras will have the {@link 250 * android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen content}, 251 * and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE client 252 * state}. Once you complete your authentication flow you should set the activity 253 * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated 254 * {@link Dataset dataset} or a fully-populated {@link FillResponse response} by 255 * setting it to the {@link 256 * android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra. If you 257 * provide a dataset in the result, it will replace the authenticated dataset and 258 * will be immediately filled in. If you provide a response, it will replace the 259 * current response and the UI will be refreshed. For example, if you provided 260 * credit card information without the CVV for the data set in the {@link FillResponse 261 * response} then the returned data set should contain the CVV entry. 262 * 263 * <p><b>Note:</b> Do not make the provided pending intent 264 * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the 265 * platform needs to fill in the authentication arguments. 266 * 267 * @param authentication Intent to an activity with your authentication flow. 268 * @return this builder. 269 * 270 * @see android.app.PendingIntent 271 */ setAuthentication(@ullable IntentSender authentication)272 public @NonNull Builder setAuthentication(@Nullable IntentSender authentication) { 273 throwIfDestroyed(); 274 mAuthentication = authentication; 275 return this; 276 } 277 278 /** 279 * Sets the id for the dataset so its usage can be tracked. 280 * 281 * <p>Dataset usage can be tracked for 2 purposes: 282 * 283 * <ul> 284 * <li>For statistical purposes, the service can call 285 * {@link AutofillService#getFillEventHistory()} when handling {@link 286 * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)} 287 * calls. 288 * <li>For normal autofill workflow, the service can call 289 * {@link SaveRequest#getDatasetIds()} when handling 290 * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} calls. 291 * </ul> 292 * 293 * @param id id for this dataset or {@code null} to unset. 294 * 295 * @return this builder. 296 */ setId(@ullable String id)297 public @NonNull Builder setId(@Nullable String id) { 298 throwIfDestroyed(); 299 mId = id; 300 return this; 301 } 302 303 /** 304 * Sets the value of a field. 305 * 306 * <b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, this method would 307 * throw an {@link IllegalStateException} if this builder was constructed without a 308 * {@link RemoteViews presentation}. Android {@link android.os.Build.VERSION_CODES#P} and 309 * higher removed this restriction because datasets used as an 310 * {@link android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT 311 * authentication result} do not need a presentation. But if you don't set the presentation 312 * in the constructor in a dataset that is meant to be shown to the user, the autofill UI 313 * for this field will not be displayed. 314 * 315 * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and 316 * higher, datasets that require authentication can be also be filtered by passing a 317 * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter. 318 * 319 * @param id id returned by {@link 320 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 321 * @param value value to be autofilled. Pass {@code null} if you do not have the value 322 * but the target view is a logical part of the dataset. For example, if 323 * the dataset needs authentication and you have no access to the value. 324 * @return this builder. 325 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value)326 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value) { 327 throwIfDestroyed(); 328 setLifeTheUniverseAndEverything(id, value, null, null); 329 return this; 330 } 331 332 /** 333 * Sets the value of a field, using a custom {@link RemoteViews presentation} to 334 * visualize it. 335 * 336 * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and 337 * higher, datasets that require authentication can be also be filtered by passing a 338 * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter. 339 * 340 * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color 341 * or background color: Autofill on different platforms may have different themes. 342 * 343 * @param id id returned by {@link 344 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 345 * @param value the value to be autofilled. Pass {@code null} if you do not have the value 346 * but the target view is a logical part of the dataset. For example, if 347 * the dataset needs authentication and you have no access to the value. 348 * @param presentation the presentation used to visualize this field. 349 * @return this builder. 350 * 351 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value, @NonNull RemoteViews presentation)352 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, 353 @NonNull RemoteViews presentation) { 354 throwIfDestroyed(); 355 Preconditions.checkNotNull(presentation, "presentation cannot be null"); 356 setLifeTheUniverseAndEverything(id, value, presentation, null); 357 return this; 358 } 359 360 /** 361 * Sets the value of a field using an <a href="#Filtering">explicit filter</a>. 362 * 363 * <p>This method is typically used when the dataset requires authentication and the service 364 * does not know its value but wants to hide the dataset after the user enters a minimum 365 * number of characters. For example, if the dataset represents a credit card number and the 366 * service does not want to show the "Tap to authenticate" message until the user tapped 367 * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}. 368 * 369 * <p><b>Note:</b> If the dataset requires authentication but the service knows its text 370 * value it's easier to filter by calling {@link #setValue(AutofillId, AutofillValue)} and 371 * use the value to filter. 372 * 373 * @param id id returned by {@link 374 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 375 * @param value the value to be autofilled. Pass {@code null} if you do not have the value 376 * but the target view is a logical part of the dataset. For example, if 377 * the dataset needs authentication and you have no access to the value. 378 * @param filter regex used to determine if the dataset should be shown in the autofill UI; 379 * when {@code null}, it disables filtering on that dataset (this is the recommended 380 * approach when {@code value} is not {@code null} and field contains sensitive data 381 * such as passwords). 382 * 383 * @return this builder. 384 * @throws IllegalStateException if the builder was constructed without a 385 * {@link RemoteViews presentation}. 386 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter)387 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, 388 @Nullable Pattern filter) { 389 throwIfDestroyed(); 390 Preconditions.checkState(mPresentation != null, 391 "Dataset presentation not set on constructor"); 392 setLifeTheUniverseAndEverything(id, value, null, new DatasetFieldFilter(filter)); 393 return this; 394 } 395 396 /** 397 * Sets the value of a field, using a custom {@link RemoteViews presentation} to 398 * visualize it and a <a href="#Filtering">explicit filter</a>. 399 * 400 * <p>This method is typically used when the dataset requires authentication and the service 401 * does not know its value but wants to hide the dataset after the user enters a minimum 402 * number of characters. For example, if the dataset represents a credit card number and the 403 * service does not want to show the "Tap to authenticate" message until the user tapped 404 * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}. 405 * 406 * <p><b>Note:</b> If the dataset requires authentication but the service knows its text 407 * value it's easier to filter by calling 408 * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter. 409 * 410 * @param id id returned by {@link 411 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 412 * @param value the value to be autofilled. Pass {@code null} if you do not have the value 413 * but the target view is a logical part of the dataset. For example, if 414 * the dataset needs authentication and you have no access to the value. 415 * @param filter regex used to determine if the dataset should be shown in the autofill UI; 416 * when {@code null}, it disables filtering on that dataset (this is the recommended 417 * approach when {@code value} is not {@code null} and field contains sensitive data 418 * such as passwords). 419 * @param presentation the presentation used to visualize this field. 420 * 421 * @return this builder. 422 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter, @NonNull RemoteViews presentation)423 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, 424 @Nullable Pattern filter, @NonNull RemoteViews presentation) { 425 throwIfDestroyed(); 426 Preconditions.checkNotNull(presentation, "presentation cannot be null"); 427 setLifeTheUniverseAndEverything(id, value, presentation, 428 new DatasetFieldFilter(filter)); 429 return this; 430 } 431 setLifeTheUniverseAndEverything(@onNull AutofillId id, @Nullable AutofillValue value, @Nullable RemoteViews presentation, @Nullable DatasetFieldFilter filter)432 private void setLifeTheUniverseAndEverything(@NonNull AutofillId id, 433 @Nullable AutofillValue value, @Nullable RemoteViews presentation, 434 @Nullable DatasetFieldFilter filter) { 435 Preconditions.checkNotNull(id, "id cannot be null"); 436 if (mFieldIds != null) { 437 final int existingIdx = mFieldIds.indexOf(id); 438 if (existingIdx >= 0) { 439 mFieldValues.set(existingIdx, value); 440 mFieldPresentations.set(existingIdx, presentation); 441 mFieldFilters.set(existingIdx, filter); 442 return; 443 } 444 } else { 445 mFieldIds = new ArrayList<>(); 446 mFieldValues = new ArrayList<>(); 447 mFieldPresentations = new ArrayList<>(); 448 mFieldFilters = new ArrayList<>(); 449 } 450 mFieldIds.add(id); 451 mFieldValues.add(value); 452 mFieldPresentations.add(presentation); 453 mFieldFilters.add(filter); 454 } 455 456 /** 457 * Creates a new {@link Dataset} instance. 458 * 459 * <p>You should not interact with this builder once this method is called. 460 * 461 * @throws IllegalStateException if no field was set (through 462 * {@link #setValue(AutofillId, AutofillValue)} or 463 * {@link #setValue(AutofillId, AutofillValue, RemoteViews)}). 464 * 465 * @return The built dataset. 466 */ build()467 public @NonNull Dataset build() { 468 throwIfDestroyed(); 469 mDestroyed = true; 470 if (mFieldIds == null) { 471 throw new IllegalStateException("at least one value must be set"); 472 } 473 return new Dataset(this); 474 } 475 throwIfDestroyed()476 private void throwIfDestroyed() { 477 if (mDestroyed) { 478 throw new IllegalStateException("Already called #build()"); 479 } 480 } 481 } 482 483 ///////////////////////////////////// 484 // Parcelable "contract" methods. // 485 ///////////////////////////////////// 486 487 @Override describeContents()488 public int describeContents() { 489 return 0; 490 } 491 492 @Override writeToParcel(Parcel parcel, int flags)493 public void writeToParcel(Parcel parcel, int flags) { 494 parcel.writeParcelable(mPresentation, flags); 495 parcel.writeTypedList(mFieldIds, flags); 496 parcel.writeTypedList(mFieldValues, flags); 497 parcel.writeTypedList(mFieldPresentations, flags); 498 parcel.writeTypedList(mFieldFilters, flags); 499 parcel.writeParcelable(mAuthentication, flags); 500 parcel.writeString(mId); 501 } 502 503 public static final @android.annotation.NonNull Creator<Dataset> CREATOR = new Creator<Dataset>() { 504 @Override 505 public Dataset createFromParcel(Parcel parcel) { 506 // Always go through the builder to ensure the data ingested by 507 // the system obeys the contract of the builder to avoid attacks 508 // using specially crafted parcels. 509 final RemoteViews presentation = parcel.readParcelable(null); 510 final Builder builder = (presentation == null) 511 ? new Builder() 512 : new Builder(presentation); 513 final ArrayList<AutofillId> ids = 514 parcel.createTypedArrayList(AutofillId.CREATOR); 515 final ArrayList<AutofillValue> values = 516 parcel.createTypedArrayList(AutofillValue.CREATOR); 517 final ArrayList<RemoteViews> presentations = 518 parcel.createTypedArrayList(RemoteViews.CREATOR); 519 final ArrayList<DatasetFieldFilter> filters = 520 parcel.createTypedArrayList(DatasetFieldFilter.CREATOR); 521 for (int i = 0; i < ids.size(); i++) { 522 final AutofillId id = ids.get(i); 523 final AutofillValue value = values.get(i); 524 final RemoteViews fieldPresentation = presentations.get(i); 525 final DatasetFieldFilter filter = filters.get(i); 526 builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation, filter); 527 } 528 builder.setAuthentication(parcel.readParcelable(null)); 529 builder.setId(parcel.readString()); 530 return builder.build(); 531 } 532 533 @Override 534 public Dataset[] newArray(int size) { 535 return new Dataset[size]; 536 } 537 }; 538 539 /** 540 * Helper class used to indicate when the service explicitly set a {@link Pattern} filter for a 541 * dataset field‐ we cannot use a {@link Pattern} directly because then we wouldn't be 542 * able to differentiate whether the service explicitly passed a {@code null} filter to disable 543 * filter, or when it called the methods that does not take a filter {@link Pattern}. 544 * 545 * @hide 546 */ 547 public static final class DatasetFieldFilter implements Parcelable { 548 549 @Nullable 550 public final Pattern pattern; 551 DatasetFieldFilter(@ullable Pattern pattern)552 private DatasetFieldFilter(@Nullable Pattern pattern) { 553 this.pattern = pattern; 554 } 555 556 @Override toString()557 public String toString() { 558 if (!sDebug) return super.toString(); 559 560 // Cannot log pattern because it could contain PII 561 return pattern == null ? "null" : pattern.pattern().length() + "_chars"; 562 } 563 564 @Override describeContents()565 public int describeContents() { 566 return 0; 567 } 568 569 @Override writeToParcel(Parcel parcel, int flags)570 public void writeToParcel(Parcel parcel, int flags) { 571 parcel.writeSerializable(pattern); 572 } 573 574 @SuppressWarnings("hiding") 575 public static final @android.annotation.NonNull Creator<DatasetFieldFilter> CREATOR = 576 new Creator<DatasetFieldFilter>() { 577 578 @Override 579 public DatasetFieldFilter createFromParcel(Parcel parcel) { 580 return new DatasetFieldFilter((Pattern) parcel.readSerializable()); 581 } 582 583 @Override 584 public DatasetFieldFilter[] newArray(int size) { 585 return new DatasetFieldFilter[size]; 586 } 587 }; 588 } 589 } 590