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.IntDef;
21 import android.annotation.IntRange;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.graphics.Point;
25 import android.graphics.pdf.flags.Flags;
26 import android.graphics.pdf.utils.Preconditions;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 
30 import java.lang.annotation.Retention;
31 import java.lang.annotation.RetentionPolicy;
32 import java.util.Arrays;
33 import java.util.Objects;
34 
35 /**
36  * Record of a form filling operation that has been executed on a single form field in a PDF.
37  * Contains the minimum amount of data required to replicate the action on the form.
38  *
39  * @see <a
40  * href="https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf">PDF
41  * 32000-1:2008</a>
42  */
43 @FlaggedApi(Flags.FLAG_ENABLE_FORM_FILLING)
44 public final class FormEditRecord implements Parcelable {
45 
46     /** Indicates a click on a clickable form widget */
47     public static final int EDIT_TYPE_CLICK = 0;
48     /** Represents setting indices on a combobox or listbox form widget */
49     public static final int EDIT_TYPE_SET_INDICES = 1;
50     /** Represents setting text on a text field or editable combobox form widget */
51     public static final int EDIT_TYPE_SET_TEXT = 2;
52     @NonNull
53     public static final Creator<FormEditRecord> CREATOR =
54             new Creator<FormEditRecord>() {
55                 @Override
56                 public FormEditRecord createFromParcel(Parcel in) {
57                     return new FormEditRecord(in);
58                 }
59 
60                 @Override
61                 public FormEditRecord[] newArray(int size) {
62                     return new FormEditRecord[size];
63                 }
64             };
65     /** Represents the page number on which the edit occurred */
66     private final int mPageNumber;
67 
68     /** Represents the index of the widget that was edited. */
69     private final int mWidgetIndex;
70 
71     private final @EditType int mType;
72 
73     @Nullable
74     private final Point mClickPoint;
75 
76     @NonNull
77     private final int[] mSelectedIndices;
78 
79     @Nullable
80     private final String mText;
81 
82     /** Private, use {@link Builder}. */
FormEditRecord( int pageNumber, int widgetIndex, @EditType int type, @Nullable Point clickPoint, @Nullable int[] selectedIndices, @Nullable String text)83     private FormEditRecord(
84             int pageNumber,
85             int widgetIndex,
86             @EditType int type,
87             @Nullable Point clickPoint,
88             @Nullable int[] selectedIndices,
89             @Nullable String text) {
90         this.mPageNumber = pageNumber;
91         this.mWidgetIndex = widgetIndex;
92         this.mType = type;
93         this.mClickPoint = clickPoint;
94         this.mSelectedIndices = Objects.requireNonNullElseGet(selectedIndices, () -> new int[0]);
95         this.mText = text;
96     }
97 
FormEditRecord(@onNull Parcel in)98     private FormEditRecord(@NonNull Parcel in) {
99         mPageNumber = in.readInt();
100         mWidgetIndex = in.readInt();
101         mType = in.readInt();
102         mClickPoint = in.readParcelable(Point.class.getClassLoader());
103 
104         int selectedIndicesSize = in.readInt();
105         mSelectedIndices = new int[selectedIndicesSize];
106         in.readIntArray(mSelectedIndices);
107 
108         mText = in.readString();
109     }
110 
111     /**
112      * @return the page on which the edit occurred
113      */
114     @IntRange(from = 0)
getPageNumber()115     public int getPageNumber() {
116         return mPageNumber;
117     }
118 
119     /**
120      * @return the index of the widget within the page's "Annot" array in the PDF document
121      */
122     @IntRange(from = 0)
getWidgetIndex()123     public int getWidgetIndex() {
124         return mWidgetIndex;
125     }
126 
127     /** @return the type of the edit */
128     @EditType
getType()129     public int getType() {
130         return mType;
131     }
132 
133     /**
134      * @return the point on which the user tapped, if this record is of type {@link
135      * #EDIT_TYPE_CLICK}, else null
136      */
137     @Nullable
getClickPoint()138     public Point getClickPoint() {
139         return mClickPoint;
140     }
141 
142     /**
143      * @return the selected indices in the choice widget, if this record is of type {@link
144      * #EDIT_TYPE_SET_INDICES}, else an empty array
145      */
146     @NonNull
getSelectedIndices()147     public int[] getSelectedIndices() {
148         return mSelectedIndices;
149     }
150 
151     /**
152      * @return the text input by the user, if this record is of type {@link #EDIT_TYPE_SET_TEXT},
153      * else null
154      */
155     @Nullable
getText()156     public String getText() {
157         return mText;
158     }
159 
160     @Override
describeContents()161     public int describeContents() {
162         return 0;
163     }
164 
165     @Override
writeToParcel(@onNull Parcel dest, int flags)166     public void writeToParcel(@NonNull Parcel dest, int flags) {
167         dest.writeInt(mPageNumber);
168         dest.writeInt(mWidgetIndex);
169         dest.writeInt(mType);
170         dest.writeParcelable(mClickPoint, flags);
171         dest.writeInt(mSelectedIndices.length);
172         dest.writeIntArray(mSelectedIndices);
173         dest.writeString(mText);
174     }
175 
176     @Override
equals(Object obj)177     public boolean equals(Object obj) {
178         if (obj == this) {
179             return true;
180         }
181         if (!(obj instanceof FormEditRecord formEditRecord)) {
182             return false;
183         }
184 
185         return mPageNumber == formEditRecord.mPageNumber
186                 && mWidgetIndex == formEditRecord.mWidgetIndex
187                 && mType == formEditRecord.mType
188                 && Objects.equals(mClickPoint, formEditRecord.mClickPoint)
189                 && Objects.equals(mText, formEditRecord.mText)
190                 && Arrays.equals(mSelectedIndices, formEditRecord.mSelectedIndices);
191     }
192 
193     @Override
hashCode()194     public int hashCode() {
195         return Objects.hash(mPageNumber, mWidgetIndex, mType, mClickPoint,
196                 Arrays.hashCode(mSelectedIndices), mText);
197     }
198 
199     /**
200      * Form edit operation type
201      *
202      * @hide
203      */
204     @IntDef({
205             EDIT_TYPE_CLICK,
206             EDIT_TYPE_SET_INDICES,
207             EDIT_TYPE_SET_TEXT
208     })
209     @Retention(RetentionPolicy.SOURCE)
210     public @interface EditType {
211     }
212 
213     /** Builder for {@link FormEditRecord} */
214     public static final class Builder {
215         private final @EditType int mType;
216 
217         private final int mPageNumber;
218         private final int mWidgetIndex;
219 
220         @Nullable
221         private Point mClickPoint = null;
222 
223         @Nullable
224         private int[] mSelectedIndices = null;
225 
226         @Nullable
227         private String mText = null;
228 
229         /**
230          * Creates a new instance.
231          *
232          * @param type        the type of {@link FormEditRecord} to create
233          * @param pageNumber  the page number of which the record is
234          * @param widgetIndex the index of the widget within the page's "Annot" array in the PDF
235          * @throws IllegalArgumentException if a negative page number or widget index is provided
236          */
Builder( @ditType int type, @IntRange(from = 0) int pageNumber, @IntRange(from = 0) int widgetIndex)237         public Builder(
238                 @EditType int type,
239                 @IntRange(from = 0) int pageNumber,
240                 @IntRange(from = 0) int widgetIndex) {
241             Preconditions.checkArgument(pageNumber >= 0, "Invalid pageNumber.");
242             Preconditions.checkArgument(widgetIndex >= 0, "Invalid widgetIndex.");
243             this.mType = type;
244             this.mPageNumber = pageNumber;
245             this.mWidgetIndex = widgetIndex;
246         }
247 
248         /**
249          * Builds this record
250          *
251          * @throws NullPointerException if the click point is not provided for a click type record,
252          *                              if the selected indices are not provided for a set indices
253          *                              type record, or if the text is
254          *                              not provided for a set text type record
255          */
256         @NonNull
build()257         public FormEditRecord build() {
258             switch (mType) {
259                 case EDIT_TYPE_CLICK:
260                     Preconditions.checkNotNull(
261                             mClickPoint, "Cannot construct CLICK record without clickPoint.");
262                     break;
263                 case EDIT_TYPE_SET_INDICES:
264                     Preconditions.checkNotNull(
265                             mSelectedIndices,
266                             "Cannot construct SET_INDICES record without selectedIndices.");
267                     break;
268                 case EDIT_TYPE_SET_TEXT:
269                     Preconditions.checkNotNull(
270                             mText, "Cannot construct SET_TEXT record without text.");
271                     break;
272             }
273             return new FormEditRecord(
274                     mPageNumber, mWidgetIndex, mType, mClickPoint, mSelectedIndices, mText);
275         }
276 
277         /**
278          * Sets the click point for this record
279          *
280          * @throws IllegalArgumentException if this is not a click type record
281          */
282         @NonNull
setClickPoint(@ullable Point clickPoint)283         public Builder setClickPoint(@Nullable Point clickPoint) {
284             Preconditions.checkArgument(
285                     mType == EDIT_TYPE_CLICK, "Cannot set clickPoint on a record of this type");
286             Preconditions.checkNotNull(clickPoint, "Click point cannot be null");
287             this.mClickPoint = clickPoint;
288             return this;
289         }
290 
291         /**
292          * Sets the selected indices for this record
293          *
294          * @throws IllegalArgumentException if this is not a set indices type record
295          */
296         @NonNull
setSelectedIndices(@ullable int[] selectedIndices)297         public Builder setSelectedIndices(@Nullable int[] selectedIndices) {
298             Preconditions.checkArgument(
299                     mType == EDIT_TYPE_SET_INDICES,
300                     "Cannot set selectedIndices on a record of this type.");
301             this.mSelectedIndices = selectedIndices;
302             return this;
303         }
304 
305         /**
306          * Sets the text for this record
307          *
308          * @throws IllegalArgumentException if this is not a set text type record
309          */
310         @NonNull
setText(@ullable String text)311         public Builder setText(@Nullable String text) {
312             Preconditions.checkArgument(
313                     mType == EDIT_TYPE_SET_TEXT, "Cannot set text on a record of this type");
314             this.mText = text;
315             return this;
316         }
317     }
318 }
319