1 /* 2 * Copyright (C) 2024 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.graphics.pdf.models; 18 19 import android.annotation.FlaggedApi; 20 import android.annotation.FloatRange; 21 import android.annotation.IntDef; 22 import android.annotation.IntRange; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.graphics.Rect; 26 import android.graphics.pdf.flags.Flags; 27 import android.graphics.pdf.utils.Preconditions; 28 import android.os.Parcel; 29 import android.os.Parcelable; 30 31 import java.lang.annotation.Retention; 32 import java.lang.annotation.RetentionPolicy; 33 import java.util.ArrayList; 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.Objects; 37 38 /** 39 * Information about a form widget of a PDF document. 40 * 41 * @see <a 42 * href="https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf">PDF 43 * 32000-1:2008</a> 44 */ 45 @FlaggedApi(Flags.FLAG_ENABLE_FORM_FILLING) 46 public final class FormWidgetInfo implements Parcelable { 47 48 /** Represents a form widget type that is unknown */ 49 public static final int WIDGET_TYPE_UNKNOWN = 0; 50 /** Represents a push button type form widget */ 51 public static final int WIDGET_TYPE_PUSHBUTTON = 1; 52 /** Represents a checkbox type form widget */ 53 public static final int WIDGET_TYPE_CHECKBOX = 2; 54 /** Represents a radio button type form widget */ 55 public static final int WIDGET_TYPE_RADIOBUTTON = 3; 56 /** Represents a combobox type form widget */ 57 public static final int WIDGET_TYPE_COMBOBOX = 4; 58 /** Represents a listbox type form widget */ 59 public static final int WIDGET_TYPE_LISTBOX = 5; 60 /** Represents a text field type form widget */ 61 public static final int WIDGET_TYPE_TEXTFIELD = 6; 62 /** Represents a signature type form widget */ 63 public static final int WIDGET_TYPE_SIGNATURE = 7; 64 @NonNull 65 public static final Creator<FormWidgetInfo> CREATOR = 66 new Creator<>() { 67 @Override 68 public FormWidgetInfo createFromParcel(Parcel in) { 69 return new FormWidgetInfo(in); 70 } 71 72 @Override 73 public FormWidgetInfo[] newArray(int size) { 74 return new FormWidgetInfo[size]; 75 } 76 }; 77 private final @WidgetType int mWidgetType; 78 private final int mWidgetIndex; 79 private final Rect mWidgetRect; 80 private final boolean mReadOnly; 81 private final String mTextValue; 82 private final String mAccessibilityLabel; 83 private final boolean mEditableText; // Combobox only. 84 private final boolean mMultiSelect; // Listbox only. 85 private final boolean mMultiLineText; // Text Field only. 86 private final int mMaxLength; // Text Field only. 87 private final float mFontSize; // Editable Text only. 88 private final List<ListItem> mListItems; // Combo/Listbox only. 89 90 /** 91 * Creates a new instance 92 * 93 * @hide 94 */ FormWidgetInfo( @idgetType int widgetType, int widgetIndex, @NonNull Rect widgetRect, boolean readOnly, @Nullable String textValue, @Nullable String accessibilityLabel, boolean editableText, boolean multiSelect, boolean multiLineText, int maxLength, float fontSize, List<ListItem> listItems)95 public FormWidgetInfo( 96 @WidgetType int widgetType, 97 int widgetIndex, 98 @NonNull Rect widgetRect, 99 boolean readOnly, 100 @Nullable String textValue, 101 @Nullable String accessibilityLabel, 102 boolean editableText, 103 boolean multiSelect, 104 boolean multiLineText, 105 int maxLength, 106 float fontSize, 107 List<ListItem> listItems) { 108 this.mWidgetType = widgetType; 109 this.mWidgetIndex = widgetIndex; 110 this.mWidgetRect = widgetRect; 111 this.mReadOnly = readOnly; 112 this.mTextValue = textValue; 113 this.mAccessibilityLabel = accessibilityLabel; 114 this.mEditableText = editableText; 115 this.mMultiSelect = multiSelect; 116 this.mMultiLineText = multiLineText; 117 this.mMaxLength = maxLength; 118 this.mFontSize = fontSize; 119 // Defensive copy 120 this.mListItems = Collections.unmodifiableList(new ArrayList<>(listItems)); 121 } 122 FormWidgetInfo(Parcel in)123 private FormWidgetInfo(Parcel in) { 124 mWidgetType = in.readInt(); 125 mWidgetIndex = in.readInt(); 126 mWidgetRect = in.readParcelable(Rect.class.getClassLoader()); 127 mReadOnly = in.readInt() != 0; 128 mTextValue = in.readString(); 129 mAccessibilityLabel = in.readString(); 130 mEditableText = in.readInt() != 0; 131 mMultiSelect = in.readInt() != 0; 132 mMultiLineText = in.readInt() != 0; 133 mMaxLength = in.readInt(); 134 mFontSize = in.readFloat(); 135 ArrayList<ListItem> listItems = new ArrayList<>(); 136 in.readTypedList(listItems, ListItem.CREATOR); 137 mListItems = Collections.unmodifiableList(listItems); 138 } 139 140 /** Returns the type of this widget */ 141 @WidgetType getWidgetType()142 public int getWidgetType() { 143 return mWidgetType; 144 } 145 146 /** Returns the index of the widget within the page's "Annot" array in the PDF document */ 147 @IntRange(from = 0) getWidgetIndex()148 public int getWidgetIndex() { 149 return mWidgetIndex; 150 } 151 152 /** 153 * Returns the {@link Rect} in page coordinates occupied by the widget 154 */ 155 @NonNull getWidgetRect()156 public Rect getWidgetRect() { 157 return mWidgetRect; 158 } 159 160 /** Returns {@code true} if the widget is read-only */ isReadOnly()161 public boolean isReadOnly() { 162 return mReadOnly; 163 } 164 165 /** 166 * Returns the field's text value, if present 167 * 168 * <p><strong>Note:</strong> Comes from the "V" value in the annotation dictionary. See <a 169 * href="https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.7old 170 * .pdf">PDF 171 * Spec 1.7 Table 8.69</a> 172 * Table 8.69 173 */ 174 @Nullable getTextValue()175 public String getTextValue() { 176 return mTextValue; 177 } 178 179 /** 180 * Returns the field's accessibility label, if present 181 * 182 * <p><strong>Note:</strong> Comes from the "TU" value in the annotation dictionary, if present, 183 * or else the "T" value. See PDF Spec 1.7 Table 8.69 184 */ 185 @Nullable getAccessibilityLabel()186 public String getAccessibilityLabel() { 187 return mAccessibilityLabel; 188 } 189 190 /** Returns {@code true} if the widget is editable text */ isEditableText()191 public boolean isEditableText() { 192 return mEditableText; 193 } 194 195 /** 196 * Returns {@code true} if the widget supports selecting multiple values 197 */ isMultiSelect()198 public boolean isMultiSelect() { 199 return mMultiSelect; 200 } 201 202 203 /** 204 * Returns true if the widget supports multiple lines of text input 205 */ isMultiLineText()206 public boolean isMultiLineText() { 207 return mMultiLineText; 208 } 209 210 /** 211 * Returns the maximum length of text supported by a text input widget, or -1 for text inputs 212 * without a maximum length and widgets that are not text inputs. 213 */ 214 @IntRange(from = -1) getMaxLength()215 public int getMaxLength() { 216 return mMaxLength; 217 } 218 219 /** 220 * Returns the font size in pixels for text input, or 0 for text inputs without a specified font 221 * size and widgets that are not text inputs. 222 */ 223 @FloatRange(from = 0f) getFontSize()224 public float getFontSize() { 225 return mFontSize; 226 } 227 228 /** 229 * Returns the list of choice options in the order that it was passed in, or an empty list for 230 * widgets without choice options. 231 */ 232 @NonNull getListItems()233 public List<ListItem> getListItems() { 234 return mListItems; 235 } 236 237 @Override hashCode()238 public int hashCode() { 239 return Objects.hash( 240 mWidgetType, 241 mWidgetIndex, 242 mWidgetRect, 243 mReadOnly, 244 mTextValue, 245 mAccessibilityLabel, 246 mEditableText, 247 mMultiSelect, 248 mMultiLineText, 249 mMaxLength, 250 mFontSize, 251 mListItems); 252 } 253 254 @Override equals(Object obj)255 public boolean equals(Object obj) { 256 if (obj instanceof FormWidgetInfo other) { 257 return mWidgetType == other.mWidgetType 258 && mWidgetIndex == other.mWidgetIndex 259 && Objects.equals(mWidgetRect, other.mWidgetRect) 260 && mReadOnly == other.mReadOnly 261 && Objects.equals(mTextValue, other.mTextValue) 262 && Objects.equals(mAccessibilityLabel, other.mAccessibilityLabel) 263 && mEditableText == other.mEditableText 264 && mMultiSelect == other.mMultiSelect 265 && mMultiLineText == other.mMultiLineText 266 && mMaxLength == other.mMaxLength 267 && mFontSize == other.mFontSize 268 && mListItems.equals(other.mListItems); 269 } 270 return false; 271 } 272 273 @Override toString()274 public String toString() { 275 return "FormWidgetInfo{" 276 + "\n\ttype=" + mWidgetType + "\n\tindex=" + mWidgetIndex + "\n\trect=" 277 + mWidgetRect + "\n\treadOnly=" + mReadOnly + "\n\ttextValue=" + mTextValue 278 + "\n\taccessibilityLabel=" + mAccessibilityLabel + "\n\teditableText=" 279 + mEditableText + "\n\tmultiSelect=" + mMultiSelect + "\n\tmultiLineText=" 280 + mMultiLineText + "\n\tmaxLength=" + mMaxLength + "\n\tfontSize=" + mFontSize 281 + "\n\tmChoiceOptions=" + mListItems + "\n}"; 282 } 283 284 @Override describeContents()285 public int describeContents() { 286 return 0; 287 } 288 289 @Override writeToParcel(@onNull Parcel dest, int flags)290 public void writeToParcel(@NonNull Parcel dest, int flags) { 291 dest.writeInt(mWidgetType); 292 dest.writeInt(mWidgetIndex); 293 dest.writeParcelable(mWidgetRect, flags); 294 dest.writeInt(mReadOnly ? 1 : 0); 295 dest.writeString(mTextValue); 296 dest.writeString(mAccessibilityLabel); 297 dest.writeInt(mEditableText ? 1 : 0); 298 dest.writeInt(mMultiSelect ? 1 : 0); 299 dest.writeInt(mMultiLineText ? 1 : 0); 300 dest.writeInt(mMaxLength); 301 dest.writeFloat(mFontSize); 302 dest.writeTypedList(mListItems); 303 } 304 305 /** 306 * Represents the type of a form widget 307 * 308 * @hide 309 */ 310 @IntDef({ 311 WIDGET_TYPE_UNKNOWN, 312 WIDGET_TYPE_PUSHBUTTON, 313 WIDGET_TYPE_CHECKBOX, 314 WIDGET_TYPE_RADIOBUTTON, 315 WIDGET_TYPE_COMBOBOX, 316 WIDGET_TYPE_LISTBOX, 317 WIDGET_TYPE_TEXTFIELD, 318 WIDGET_TYPE_SIGNATURE 319 }) 320 @Retention(RetentionPolicy.SOURCE) 321 public @interface WidgetType { 322 323 } 324 325 /** Builder for {@link FormWidgetInfo} */ 326 public static final class Builder { 327 private final @WidgetType int mWidgetType; 328 private final int mWidgetIndex; 329 private final Rect mWidgetRect; 330 private final String mTextValue; 331 private final String mAccessibilityLabel; 332 private boolean mReadOnly = false; 333 private boolean mEditableText = false; // Combobox only. 334 private boolean mMultiSelect = false; // Listbox only. 335 private boolean mMultiLineText = false; // Text Field only. 336 private int mMaxLength = -1; // Text Field only. 337 private float mFontSize = 0f; // Editable Text only. 338 private List<ListItem> mListItems = List.of(); // Combo/Listbox only. 339 340 /** 341 * Creates an instance 342 * 343 * @param widgetType the type of widget 344 * @param widgetIndex the index of the widget in the page's "Annot" array in the PDF 345 * @param widgetRect the {@link Rect} in page coordinates occupied by the widget 346 * @param textValue the widget's text value 347 * @param accessibilityLabel the field's accessibility label 348 * @throws NullPointerException if any of {@code widgetRect}, {@code textValue}, or {@code 349 * accessibilityLabel} are null 350 */ Builder( @idgetType int widgetType, @IntRange(from = 0) int widgetIndex, @NonNull Rect widgetRect, @NonNull String textValue, @NonNull String accessibilityLabel)351 public Builder( 352 @WidgetType int widgetType, 353 @IntRange(from = 0) int widgetIndex, 354 @NonNull Rect widgetRect, 355 @NonNull String textValue, 356 @NonNull String accessibilityLabel) { 357 mWidgetType = widgetType; 358 mWidgetIndex = widgetIndex; 359 mWidgetRect = Preconditions.checkNotNull(widgetRect, "widgetRect cannot be null"); 360 mTextValue = Preconditions.checkNotNull(textValue, "textValue cannot be null"); 361 mAccessibilityLabel = Preconditions.checkNotNull(accessibilityLabel, 362 "accessibilityLabel cannot be null"); 363 } 364 365 /** Sets whether this widget is read-only */ 366 @NonNull setReadOnly(boolean readOnly)367 public Builder setReadOnly(boolean readOnly) { 368 mReadOnly = readOnly; 369 return this; 370 } 371 372 /** 373 * Sets whether this widget contains editable text. Only supported for comboboxes and 374 * text fields 375 * 376 * @throws IllegalArgumentException if this is not a combobox or text field type widget 377 */ 378 @NonNull setEditableText(boolean editableText)379 public Builder setEditableText(boolean editableText) { 380 Preconditions.checkArgument(mWidgetType == WIDGET_TYPE_COMBOBOX 381 || mWidgetType == WIDGET_TYPE_TEXTFIELD, 382 "Editable text is only supported on comboboxes and text fields"); 383 mEditableText = editableText; 384 return this; 385 } 386 387 /** 388 * Sets whether this widget supports multiple choice selections. Only supported for 389 * list boxes 390 * 391 * @throws IllegalArgumentException if this is not a list box 392 */ 393 @NonNull setMultiSelect(boolean multiSelect)394 public Builder setMultiSelect(boolean multiSelect) { 395 Preconditions.checkArgument(mWidgetType == WIDGET_TYPE_LISTBOX, 396 "Multi-select is only supported on list boxes"); 397 mMultiSelect = multiSelect; 398 return this; 399 } 400 401 /** 402 * Sets whether this widget supports multi-line text input. Only supported for text fields 403 * 404 * @throws IllegalArgumentException if this is not a text field 405 */ 406 @NonNull setMultiLineText(boolean multiLineText)407 public Builder setMultiLineText(boolean multiLineText) { 408 Preconditions.checkArgument(mWidgetType == WIDGET_TYPE_TEXTFIELD, 409 "Multiline text is only supported on text fields"); 410 mMultiLineText = multiLineText; 411 return this; 412 } 413 414 /** 415 * Sets the maximum character length of input text supported by this widget. Only supported 416 * for text fields 417 * 418 * @throws IllegalArgumentException if this is not a text field, or if a negative max length 419 * is supplied 420 */ 421 @NonNull setMaxLength(@ntRangefrom = 0) int maxLength)422 public Builder setMaxLength(@IntRange(from = 0) int maxLength) { 423 Preconditions.checkArgument(maxLength > 0, "Invalid max length"); 424 Preconditions.checkArgument(mWidgetType == WIDGET_TYPE_TEXTFIELD, 425 "Max length is only supported on text fields"); 426 mMaxLength = maxLength; 427 return this; 428 } 429 430 /** 431 * Sets the font size for this widget. Only supported for text fields and comboboxes 432 * 433 * @throws IllegalArgumentException if this is not a combobox or text field, or if a 434 * negative font size is supplied 435 */ 436 @NonNull setFontSize(@loatRangefrom = 0f) float fontSize)437 public Builder setFontSize(@FloatRange(from = 0f) float fontSize) { 438 Preconditions.checkArgument(fontSize > 0, "Invalid font size"); 439 Preconditions.checkArgument(mWidgetType == WIDGET_TYPE_COMBOBOX 440 || mWidgetType == WIDGET_TYPE_TEXTFIELD, 441 "Font size is only supported on comboboxes and text fields"); 442 mFontSize = fontSize; 443 return this; 444 } 445 446 /** 447 * Sets the choice options for this widget. Only supported for comboboxes and list boxes 448 * 449 * @throws IllegalArgumentException if this is not a combobox or list box 450 * @throws NullPointerException if {@code choiceOptions} is null 451 */ 452 @NonNull setListItems(@onNull List<ListItem> listItems)453 public Builder setListItems(@NonNull List<ListItem> listItems) { 454 Preconditions.checkNotNull(listItems, "choiceOptions cannot be null"); 455 Preconditions.checkArgument(mWidgetType == WIDGET_TYPE_COMBOBOX 456 || mWidgetType == WIDGET_TYPE_LISTBOX, 457 "Choice options are only supported on comboboxes and list boxes"); 458 mListItems = listItems; 459 return this; 460 } 461 462 /** Builds a {@link FormWidgetInfo} */ 463 @NonNull build()464 public FormWidgetInfo build() { 465 return new FormWidgetInfo( 466 mWidgetType, 467 mWidgetIndex, 468 mWidgetRect, 469 mReadOnly, 470 mTextValue, 471 mAccessibilityLabel, 472 mEditableText, 473 mMultiSelect, 474 mMultiLineText, 475 mMaxLength, 476 mFontSize, 477 mListItems); 478 } 479 } 480 } 481