/* * 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.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. * *
A {@link SaveInfo} is always associated with a {@link FillResponse}, and it contains at least * two pieces of information: * *
Typically, the {@link SaveInfo} contains the same {@code id}s as the {@link Dataset}: * *
* 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(); ** *
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. * *
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}: * *
* new FillResponse.Builder() * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD, * new AutofillId[] { id1, id2 }).build()) * .build(); ** *
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: * *
* 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(); ** *
The {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} can be triggered after * any of the following events: *
But it is only triggered when all conditions below are met: *
The service can also customize some aspects of the save UI affordance: *
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) { // TODO: add CTS unit tests (not integration) to assert the null cases mType = type; mRequiredIds = assertValid(requiredIds); } 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. * *
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) { // TODO: add CTS unit tests (not integration) to assert the null cases throwIfDestroyed(); mOptionalIds = assertValid(ids); return this; } /** * Sets an optional description to be shown in the UI when the user is asked to save. * *
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. */ public @NonNull Builder setDescription(@Nullable CharSequence description) { throwIfDestroyed(); mDescription = description; return this; } /** * Sets the style and listener for the negative save action. * *
This allows a fill-provider 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 will be * started. The default style is {@link #NEGATIVE_BUTTON_STYLE_CANCEL}
* * @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; } /** * Builds a new {@link SaveInfo} instance. */ public SaveInfo build() { throwIfDestroyed(); 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("]").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.writeInt(mNegativeButtonStyle); parcel.writeParcelable(mNegativeActionListener, flags); parcel.writeParcelableArray(mOptionalIds, flags); parcel.writeCharSequence(mDescription); parcel.writeInt(mFlags); } public static final Parcelable.Creator