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