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