| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.service.autofill; |
| |
| import static android.view.autofill.Helper.sDebug; |
| |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.Activity; |
| import android.content.IntentSender; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.util.DebugUtils; |
| import android.view.autofill.AutofillId; |
| import android.view.autofill.AutofillManager; |
| import android.view.autofill.AutofillValue; |
| |
| import com.android.internal.util.ArrayUtils; |
| import com.android.internal.util.Preconditions; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.Arrays; |
| |
| /** |
| * Information used to indicate that an {@link AutofillService} is interested on saving the |
| * user-inputed data for future use, through a |
| * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} |
| * call. |
| * |
| * <p>A {@link SaveInfo} is always associated with a {@link FillResponse}, and it contains at least |
| * two pieces of information: |
| * |
| * <ol> |
| * <li>The type(s) of user data (like password or credit card info) that would be saved. |
| * <li>The minimum set of views (represented by their {@link AutofillId}) that need to be changed |
| * to trigger a save request. |
| * </ol> |
| * |
| * <p>Typically, the {@link SaveInfo} contains the same {@code id}s as the {@link Dataset}: |
| * |
| * <pre class="prettyprint"> |
| * new FillResponse.Builder() |
| * .addDataset(new Dataset.Builder() |
| * .setValue(id1, AutofillValue.forText("homer"), createPresentation("homer")) // username |
| * .setValue(id2, AutofillValue.forText("D'OH!"), createPresentation("password for homer")) // password |
| * .build()) |
| * .setSaveInfo(new SaveInfo.Builder( |
| * SaveInfo.SAVE_DATA_TYPE_USERNAME | SaveInfo.SAVE_DATA_TYPE_PASSWORD, |
| * new AutofillId[] { id1, id2 }).build()) |
| * .build(); |
| * </pre> |
| * |
| * <p>The save type flags are used to display the appropriate strings in the save UI affordance. |
| * You can pass multiple values, but try to keep it short if possible. In the above example, just |
| * {@code SaveInfo.SAVE_DATA_TYPE_PASSWORD} would be enough. |
| * |
| * <p>There might be cases where the {@link AutofillService} knows how to fill the screen, |
| * but the user has no data for it. In that case, the {@link FillResponse} should contain just the |
| * {@link SaveInfo}, but no {@link Dataset Datasets}: |
| * |
| * <pre class="prettyprint"> |
| * new FillResponse.Builder() |
| * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD, |
| * new AutofillId[] { id1, id2 }).build()) |
| * .build(); |
| * </pre> |
| * |
| * <p>There might be cases where the user data in the {@link AutofillService} is enough |
| * to populate some fields but not all, and the service would still be interested on saving the |
| * other fields. In that case, the service could set the |
| * {@link SaveInfo.Builder#setOptionalIds(AutofillId[])} as well: |
| * |
| * <pre class="prettyprint"> |
| * new FillResponse.Builder() |
| * .addDataset(new Dataset.Builder() |
| * .setValue(id1, AutofillValue.forText("742 Evergreen Terrace"), |
| * createPresentation("742 Evergreen Terrace")) // street |
| * .setValue(id2, AutofillValue.forText("Springfield"), |
| * createPresentation("Springfield")) // city |
| * .build()) |
| * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_ADDRESS, |
| * new AutofillId[] { id1, id2 }) // street and city |
| * .setOptionalIds(new AutofillId[] { id3, id4 }) // state and zipcode |
| * .build()) |
| * .build(); |
| * </pre> |
| * |
| * <p>The {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} can be triggered after |
| * any of the following events: |
| * <ul> |
| * <li>The {@link Activity} finishes. |
| * <li>The app explicitly called {@link AutofillManager#commit()}. |
| * <li>All required views became invisible (if the {@link SaveInfo} was created with the |
| * {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} flag). |
| * </ul> |
| * |
| * <p>But it is only triggered when all conditions below are met: |
| * <ul> |
| * <li>The {@link SaveInfo} associated with the {@link FillResponse} is not {@code null}. |
| * <li>The {@link AutofillValue}s of all required views (as set by the {@code requiredIds} passed |
| * to the {@link SaveInfo.Builder} constructor are not empty. |
| * <li>The {@link AutofillValue} of at least one view (be it required or optional) has changed |
| * (i.e., it's neither the same value passed in a {@link Dataset}, nor the initial value |
| * presented in the view). |
| * <li>The user explicitly tapped the UI affordance asking to save data for autofill. |
| * </ul> |
| * |
| * <p>The service can also customize some aspects of the save UI affordance: |
| * <ul> |
| * <li>Add a simple subtitle by calling {@link Builder#setDescription(CharSequence)}. |
| * <li>Add a customized subtitle by calling |
| * {@link Builder#setCustomDescription(CustomDescription)}. |
| * <li>Customize the button used to reject the save request by calling |
| * {@link Builder#setNegativeAction(int, IntentSender)}. |
| * <li>Decide whether the UI should be shown based on the user input validation by calling |
| * {@link Builder#setValidator(Validator)}. |
| * </ul> |
| */ |
| public final class SaveInfo implements Parcelable { |
| |
| /** |
| * Type used when the service can save the contents of a screen, but cannot describe what |
| * the content is for. |
| */ |
| public static final int SAVE_DATA_TYPE_GENERIC = 0x0; |
| |
| /** |
| * Type used when the {@link FillResponse} represents user credentials that have a password. |
| */ |
| public static final int SAVE_DATA_TYPE_PASSWORD = 0x01; |
| |
| /** |
| * Type used on when the {@link FillResponse} represents a physical address (such as street, |
| * city, state, etc). |
| */ |
| public static final int SAVE_DATA_TYPE_ADDRESS = 0x02; |
| |
| /** |
| * Type used when the {@link FillResponse} represents a credit card. |
| */ |
| public static final int SAVE_DATA_TYPE_CREDIT_CARD = 0x04; |
| |
| /** |
| * Type used when the {@link FillResponse} represents just an username, without a password. |
| */ |
| public static final int SAVE_DATA_TYPE_USERNAME = 0x08; |
| |
| /** |
| * Type used when the {@link FillResponse} represents just an email address, without a password. |
| */ |
| public static final int SAVE_DATA_TYPE_EMAIL_ADDRESS = 0x10; |
| |
| /** |
| * Style for the negative button of the save UI to cancel the |
| * save operation. In this case, the user tapping the negative |
| * button signals that they would prefer to not save the filled |
| * content. |
| */ |
| public static final int NEGATIVE_BUTTON_STYLE_CANCEL = 0; |
| |
| /** |
| * Style for the negative button of the save UI to reject the |
| * save operation. This could be useful if the user needs to |
| * opt-in your service and the save prompt is an advertisement |
| * of the potential value you can add to the user. In this |
| * case, the user tapping the negative button sends a strong |
| * signal that the feature may not be useful and you may |
| * consider some backoff strategy. |
| */ |
| public static final int NEGATIVE_BUTTON_STYLE_REJECT = 1; |
| |
| /** @hide */ |
| @IntDef( |
| value = { |
| NEGATIVE_BUTTON_STYLE_CANCEL, |
| NEGATIVE_BUTTON_STYLE_REJECT}) |
| @Retention(RetentionPolicy.SOURCE) |
| @interface NegativeButtonStyle{} |
| |
| /** @hide */ |
| @IntDef( |
| flag = true, |
| value = { |
| SAVE_DATA_TYPE_GENERIC, |
| SAVE_DATA_TYPE_PASSWORD, |
| SAVE_DATA_TYPE_ADDRESS, |
| SAVE_DATA_TYPE_CREDIT_CARD, |
| SAVE_DATA_TYPE_USERNAME, |
| SAVE_DATA_TYPE_EMAIL_ADDRESS}) |
| @Retention(RetentionPolicy.SOURCE) |
| @interface SaveDataType{} |
| |
| /** |
| * Usually {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} |
| * is called once the {@link Activity} finishes. If this flag is set it is called once all |
| * saved views become invisible. |
| */ |
| public static final int FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE = 0x1; |
| |
| /** @hide */ |
| @IntDef( |
| flag = true, |
| value = {FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE}) |
| @Retention(RetentionPolicy.SOURCE) |
| @interface SaveInfoFlags{} |
| |
| private final @SaveDataType int mType; |
| private final @NegativeButtonStyle int mNegativeButtonStyle; |
| private final IntentSender mNegativeActionListener; |
| private final AutofillId[] mRequiredIds; |
| private final AutofillId[] mOptionalIds; |
| private final CharSequence mDescription; |
| private final int mFlags; |
| private final CustomDescription mCustomDescription; |
| private final InternalValidator mValidator; |
| |
| private SaveInfo(Builder builder) { |
| mType = builder.mType; |
| mNegativeButtonStyle = builder.mNegativeButtonStyle; |
| mNegativeActionListener = builder.mNegativeActionListener; |
| mRequiredIds = builder.mRequiredIds; |
| mOptionalIds = builder.mOptionalIds; |
| mDescription = builder.mDescription; |
| mFlags = builder.mFlags; |
| mCustomDescription = builder.mCustomDescription; |
| mValidator = builder.mValidator; |
| } |
| |
| /** @hide */ |
| public @NegativeButtonStyle int getNegativeActionStyle() { |
| return mNegativeButtonStyle; |
| } |
| |
| /** @hide */ |
| public @Nullable IntentSender getNegativeActionListener() { |
| return mNegativeActionListener; |
| } |
| |
| /** @hide */ |
| public @Nullable AutofillId[] getRequiredIds() { |
| return mRequiredIds; |
| } |
| |
| /** @hide */ |
| public @Nullable AutofillId[] getOptionalIds() { |
| return mOptionalIds; |
| } |
| |
| /** @hide */ |
| public @SaveDataType int getType() { |
| return mType; |
| } |
| |
| /** @hide */ |
| public @SaveInfoFlags int getFlags() { |
| return mFlags; |
| } |
| |
| /** @hide */ |
| public CharSequence getDescription() { |
| return mDescription; |
| } |
| |
| /** @hide */ |
| @Nullable |
| public CustomDescription getCustomDescription() { |
| return mCustomDescription; |
| } |
| |
| /** @hide */ |
| @Nullable |
| public InternalValidator getValidator() { |
| return mValidator; |
| } |
| |
| /** |
| * A builder for {@link SaveInfo} objects. |
| */ |
| public static final class Builder { |
| |
| private final @SaveDataType int mType; |
| private @NegativeButtonStyle int mNegativeButtonStyle = NEGATIVE_BUTTON_STYLE_CANCEL; |
| private IntentSender mNegativeActionListener; |
| private final AutofillId[] mRequiredIds; |
| private AutofillId[] mOptionalIds; |
| private CharSequence mDescription; |
| private boolean mDestroyed; |
| private int mFlags; |
| private CustomDescription mCustomDescription; |
| private InternalValidator mValidator; |
| |
| /** |
| * Creates a new builder. |
| * |
| * @param type the type of information the associated {@link FillResponse} represents. It |
| * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, |
| * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD}, |
| * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD}, |
| * {@link SaveInfo#SAVE_DATA_TYPE_USERNAME}, or |
| * {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}. |
| * @param requiredIds ids of all required views that will trigger a save request. |
| * |
| * <p>See {@link SaveInfo} for more info. |
| * |
| * @throws IllegalArgumentException if {@code requiredIds} is {@code null} or empty, or if |
| * it contains any {@code null} entry. |
| */ |
| public Builder(@SaveDataType int type, @NonNull AutofillId[] requiredIds) { |
| mType = type; |
| mRequiredIds = assertValid(requiredIds); |
| } |
| |
| /** |
| * Creates a new builder when no id is required. |
| * |
| * <p>When using this builder, caller must call {@link #setOptionalIds(AutofillId[])} before |
| * calling {@link #build()}. |
| * |
| * @param type the type of information the associated {@link FillResponse} represents. It |
| * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, |
| * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD}, |
| * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD}, |
| * {@link SaveInfo#SAVE_DATA_TYPE_USERNAME}, or |
| * {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}. |
| * |
| * <p>See {@link SaveInfo} for more info. |
| */ |
| public Builder(@SaveDataType int type) { |
| mType = type; |
| mRequiredIds = null; |
| } |
| |
| private AutofillId[] assertValid(AutofillId[] ids) { |
| Preconditions.checkArgument(ids != null && ids.length > 0, |
| "must have at least one id: " + Arrays.toString(ids)); |
| for (int i = 0; i < ids.length; i++) { |
| final AutofillId id = ids[i]; |
| Preconditions.checkArgument(id != null, |
| "cannot have null id: " + Arrays.toString(ids)); |
| } |
| return ids; |
| } |
| |
| /** |
| * Sets flags changing the save behavior. |
| * |
| * @param flags {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} or {@code 0}. |
| * @return This builder. |
| */ |
| public @NonNull Builder setFlags(@SaveInfoFlags int flags) { |
| throwIfDestroyed(); |
| |
| mFlags = Preconditions.checkFlagsArgument(flags, FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE); |
| return this; |
| } |
| |
| /** |
| * Sets the ids of additional, optional views the service would be interested to save. |
| * |
| * <p>See {@link SaveInfo} for more info. |
| * |
| * @param ids The ids of the optional views. |
| * @return This builder. |
| * |
| * @throws IllegalArgumentException if {@code ids} is {@code null} or empty, or if |
| * it contains any {@code null} entry. |
| */ |
| public @NonNull Builder setOptionalIds(@NonNull AutofillId[] ids) { |
| throwIfDestroyed(); |
| mOptionalIds = assertValid(ids); |
| return this; |
| } |
| |
| /** |
| * Sets an optional description to be shown in the UI when the user is asked to save. |
| * |
| * <p>Typically, it describes how the data will be stored by the service, so it can help |
| * users to decide whether they can trust the service to save their data. |
| * |
| * @param description a succint description. |
| * @return This Builder. |
| * |
| * @throws IllegalStateException if this call was made after calling |
| * {@link #setCustomDescription(CustomDescription)}. |
| */ |
| public @NonNull Builder setDescription(@Nullable CharSequence description) { |
| throwIfDestroyed(); |
| Preconditions.checkState(mCustomDescription == null, |
| "Can call setDescription() or setCustomDescription(), but not both"); |
| mDescription = description; |
| return this; |
| } |
| |
| /** |
| * Sets a custom description to be shown in the UI when the user is asked to save. |
| * |
| * <p>Typically used when the service must show more info about the object being saved, |
| * like a credit card logo, masked number, and expiration date. |
| * |
| * @param customDescription the custom description. |
| * @return This Builder. |
| * |
| * @throws IllegalStateException if this call was made after calling |
| * {@link #setDescription(CharSequence)}. |
| */ |
| public @NonNull Builder setCustomDescription(@NonNull CustomDescription customDescription) { |
| throwIfDestroyed(); |
| Preconditions.checkState(mDescription == null, |
| "Can call setDescription() or setCustomDescription(), but not both"); |
| mCustomDescription = customDescription; |
| return this; |
| } |
| |
| /** |
| * Sets the style and listener for the negative save action. |
| * |
| * <p>This allows an autofill service to customize the style and be |
| * notified when the user selects the negative action in the save |
| * UI. Note that selecting the negative action regardless of its style |
| * and listener being customized would dismiss the save UI and if a |
| * custom listener intent is provided then this intent is |
| * started. The default style is {@link #NEGATIVE_BUTTON_STYLE_CANCEL}</p> |
| * |
| * @param style The action style. |
| * @param listener The action listener. |
| * @return This builder. |
| * |
| * @see #NEGATIVE_BUTTON_STYLE_CANCEL |
| * @see #NEGATIVE_BUTTON_STYLE_REJECT |
| * |
| * @throws IllegalArgumentException If the style is invalid |
| */ |
| public @NonNull Builder setNegativeAction(@NegativeButtonStyle int style, |
| @Nullable IntentSender listener) { |
| throwIfDestroyed(); |
| if (style != NEGATIVE_BUTTON_STYLE_CANCEL |
| && style != NEGATIVE_BUTTON_STYLE_REJECT) { |
| throw new IllegalArgumentException("Invalid style: " + style); |
| } |
| mNegativeButtonStyle = style; |
| mNegativeActionListener = listener; |
| return this; |
| } |
| |
| /** |
| * Sets an object used to validate the user input - if the input is not valid, the Save UI |
| * affordance is not shown. |
| * |
| * <p>Typically used to validate credit card numbers. Examples: |
| * |
| * <p>Validator for a credit number that must have exactly 16 digits: |
| * |
| * <pre class="prettyprint"> |
| * Validator validator = new SimpleRegexValidator(ccNumberId, "^\\d{16}$") |
| * </pre> |
| * |
| * <p>Validator for a credit number that must pass a Luhn checksum and either have |
| * 16 digits, or 15 digits starting with 108: |
| * |
| * <pre class="prettyprint"> |
| * import android.service.autofill.Validators; |
| * |
| * Validator validator = |
| * and( |
| * new LuhnChecksumValidator(ccNumberId), |
| * or( |
| * new SimpleRegexValidator(ccNumberId, "^\\d{16}$"), |
| * new SimpleRegexValidator(ccNumberId, "^108\\d{12}$") |
| * ) |
| * ); |
| * </pre> |
| * |
| * <p><b>NOTE: </b>the example above is just for illustrative purposes; the same validator |
| * could be created using a single regex for the {@code OR} part: |
| * |
| * <pre class="prettyprint"> |
| * Validator validator = |
| * and( |
| * new LuhnChecksumValidator(ccNumberId), |
| * new SimpleRegexValidator(ccNumberId, "^(\\d{16}|108\\d{12})$") |
| * ); |
| * </pre> |
| * |
| * <p>Validator for a credit number contained in just 4 fields and that must have exactly |
| * 4 digits on each field: |
| * |
| * <pre class="prettyprint"> |
| * import android.service.autofill.Validators; |
| * |
| * Validator validator = |
| * and( |
| * new SimpleRegexValidator(ccNumberId1, "^\\d{4}$"), |
| * new SimpleRegexValidator(ccNumberId2, "^\\d{4}$"), |
| * new SimpleRegexValidator(ccNumberId3, "^\\d{4}$"), |
| * new SimpleRegexValidator(ccNumberId4, "^\\d{4}$") |
| * ); |
| * </pre> |
| * |
| * @param validator an implementation provided by the Android System. |
| * @return this builder. |
| * |
| * @throws IllegalArgumentException if {@code validator} is not a class provided |
| * by the Android System. |
| */ |
| public @NonNull Builder setValidator(@NonNull Validator validator) { |
| throwIfDestroyed(); |
| Preconditions.checkArgument((validator instanceof InternalValidator), |
| "not provided by Android System: " + validator); |
| mValidator = (InternalValidator) validator; |
| return this; |
| } |
| |
| /** |
| * Builds a new {@link SaveInfo} instance. |
| * |
| * @throws IllegalStateException if no |
| * {@link #SaveInfo.Builder(int, AutofillId[]) required ids} |
| * or {@link #setOptionalIds(AutofillId[]) optional ids} were set |
| */ |
| public SaveInfo build() { |
| throwIfDestroyed(); |
| Preconditions.checkState( |
| !ArrayUtils.isEmpty(mRequiredIds) || !ArrayUtils.isEmpty(mOptionalIds), |
| "must have at least one required or optional id"); |
| mDestroyed = true; |
| return new SaveInfo(this); |
| } |
| |
| private void throwIfDestroyed() { |
| if (mDestroyed) { |
| throw new IllegalStateException("Already called #build()"); |
| } |
| } |
| } |
| |
| ///////////////////////////////////// |
| // Object "contract" methods. // |
| ///////////////////////////////////// |
| @Override |
| public String toString() { |
| if (!sDebug) return super.toString(); |
| |
| return new StringBuilder("SaveInfo: [type=") |
| .append(DebugUtils.flagsToString(SaveInfo.class, "SAVE_DATA_TYPE_", mType)) |
| .append(", requiredIds=").append(Arrays.toString(mRequiredIds)) |
| .append(", optionalIds=").append(Arrays.toString(mOptionalIds)) |
| .append(", description=").append(mDescription) |
| .append(DebugUtils.flagsToString(SaveInfo.class, "NEGATIVE_BUTTON_STYLE_", |
| mNegativeButtonStyle)) |
| .append(", mFlags=").append(mFlags) |
| .append(", mCustomDescription=").append(mCustomDescription) |
| .append(", validation=").append(mValidator) |
| .append("]").toString(); |
| } |
| |
| ///////////////////////////////////// |
| // Parcelable "contract" methods. // |
| ///////////////////////////////////// |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel parcel, int flags) { |
| parcel.writeInt(mType); |
| parcel.writeParcelableArray(mRequiredIds, flags); |
| parcel.writeParcelableArray(mOptionalIds, flags); |
| parcel.writeInt(mNegativeButtonStyle); |
| parcel.writeParcelable(mNegativeActionListener, flags); |
| parcel.writeCharSequence(mDescription); |
| parcel.writeParcelable(mCustomDescription, flags); |
| parcel.writeParcelable(mValidator, flags); |
| parcel.writeInt(mFlags); |
| } |
| |
| public static final Parcelable.Creator<SaveInfo> CREATOR = new Parcelable.Creator<SaveInfo>() { |
| @Override |
| public SaveInfo createFromParcel(Parcel parcel) { |
| |
| // Always go through the builder to ensure the data ingested by |
| // the system obeys the contract of the builder to avoid attacks |
| // using specially crafted parcels. |
| final int type = parcel.readInt(); |
| final AutofillId[] requiredIds = parcel.readParcelableArray(null, AutofillId.class); |
| final Builder builder = requiredIds != null |
| ? new Builder(type, requiredIds) |
| : new Builder(type); |
| final AutofillId[] optionalIds = parcel.readParcelableArray(null, AutofillId.class); |
| if (optionalIds != null) { |
| builder.setOptionalIds(optionalIds); |
| } |
| |
| builder.setNegativeAction(parcel.readInt(), parcel.readParcelable(null)); |
| builder.setDescription(parcel.readCharSequence()); |
| final CustomDescription customDescripton = parcel.readParcelable(null); |
| if (customDescripton != null) { |
| builder.setCustomDescription(customDescripton); |
| } |
| final InternalValidator validator = parcel.readParcelable(null); |
| if (validator != null) { |
| builder.setValidator(validator); |
| } |
| builder.setFlags(parcel.readInt()); |
| return builder.build(); |
| } |
| |
| @Override |
| public SaveInfo[] newArray(int size) { |
| return new SaveInfo[size]; |
| } |
| }; |
| } |