blob: 6663f03a638303a6e4bf3193080a4fe0cd67ba5e [file] [log] [blame]
/*
* 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.DEBUG;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.IntentSender;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.autofill.AutoFillId;
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(android.app.assist.AssistStructure, Bundle, 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 of user data that would be saved (like passoword or credit card info).
* <li>The minimum set of views (represented by their {@link AutofillId}) that need to be changed
* to trigger a save request.
* </ol>
*
* Typically, the {@link SaveInfo} contains the same {@code id}s as the {@link Dataset}:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* .add(new Dataset.Builder(createPresentation())
* .setValue(id1, AutofillValue.forText("homer"))
* .setValue(id2, AutofillValue.forText("D'OH!"))
* .build())
* .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_INFO_TYPE_PASSWORD, new int[] {id1, id2})
* .build())
* .build();
* </pre>
*
* There might be cases where the {@link AutofillService} knows how to fill the
* {@link android.app.Activity}, but the user has no data for it. In that case, the
* {@link FillResponse} should contain just the {@link SaveInfo}, but no {@link Dataset}s:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_INFO_TYPE_PASSWORD, new int[] {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 this scenario, the service could set the
* {@link SaveInfo.Builder#setOptionalIds(AutofillId[])} as well:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* .add(new Dataset.Builder(createPresentation())
* .setValue(id1, AutofillValue.forText("742 Evergreen Terrace")) // street
* .setValue(id2, AutofillValue.forText("Springfield")) // city
* .build())
* .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_INFO_TYPE_ADDRESS, new int[] {id1, id2})
* .setOptionalIds(new int[] {id3, id4}) // state and zipcode
* .build())
* .build();
* </pre>
*
* The
* {@link AutofillService#onSaveRequest(android.app.assist.AssistStructure, Bundle, SaveCallback)}
* is triggered after a call to {@link AutofillManager#commit()}, but only when all conditions
* below are met:
*
* <ol>
* <li>The {@link SaveInfo} associated with the {@link FillResponse} is not {@code null}.
* <li>The {@link AutofillValue} of all required views (as set by the {@code requiredIds} passed
* to {@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 not the same value passed in a {@link Dataset}).
* <li>The user explicitly tapped the affordance asking to save data for autofill.
* </ol>
*/
public final class SaveInfo implements Parcelable {
/**
* Type used on when the service can save the contents of an activity, but cannot describe what
* the content is for.
*/
public static final int SAVE_DATA_TYPE_GENERIC = 0;
/**
* Type used when the {@link FillResponse} represents user credentials that have a password.
*/
public static final int SAVE_DATA_TYPE_PASSWORD = 1;
/**
* 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 = 2;
/**
* Type used when the {@link FillResponse} represents a credit card.
*/
public static final int SAVE_DATA_TYPE_CREDIT_CARD = 3;
private final @SaveDataType int mType;
private final CharSequence mNegativeActionTitle;
private final IntentSender mNegativeActionListener;
private final AutofillId[] mRequiredIds;
private final AutofillId[] mOptionalIds;
private final CharSequence mDescription;
/** @hide */
@IntDef({
SAVE_DATA_TYPE_GENERIC,
SAVE_DATA_TYPE_PASSWORD,
SAVE_DATA_TYPE_ADDRESS,
SAVE_DATA_TYPE_CREDIT_CARD
})
@Retention(RetentionPolicy.SOURCE)
public @interface SaveDataType {
}
private SaveInfo(Builder builder) {
mType = builder.mType;
mNegativeActionTitle = builder.mNegativeActionTitle;
mNegativeActionListener = builder.mNegativeActionListener;
mRequiredIds = builder.mRequiredIds;
mOptionalIds = builder.mOptionalIds;
mDescription = builder.mDescription;
}
/** @hide */
public @Nullable CharSequence getNegativeActionTitle() {
return mNegativeActionTitle;
}
/** @hide */
public @Nullable IntentSender getNegativeActionListener() {
return mNegativeActionListener;
}
/** @hide */
public AutofillId[] getRequiredIds() {
return mRequiredIds;
}
/** @hide */
public @Nullable AutofillId[] getOptionalIds() {
return mOptionalIds;
}
/** @hide */
public int getType() {
return mType;
}
/** @hide */
public CharSequence getDescription() {
return mDescription;
}
/**
* A builder for {@link SaveInfo} objects.
*/
public static final class Builder {
private final @SaveDataType int mType;
private CharSequence mNegativeActionTitle;
private IntentSender mNegativeActionListener;
// TODO(b/33197203): make mRequiredIds final once addSavableIds() is gone
private AutofillId[] mRequiredIds;
private AutofillId[] mOptionalIds;
private CharSequence mDescription;
private boolean mDestroyed;
/**
* Creates a new builder.
*
* @param type the type of information the associated {@link FillResponse} represents. Must
* be {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD},
* {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, or {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD};
* otherwise it will assume {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}.
* @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.
*/
public Builder(@SaveDataType int type, @NonNull AutofillId[] requiredIds) {
Preconditions.checkArgument(requiredIds != null && requiredIds.length > 0,
"must have at least on required id: " + Arrays.toString(requiredIds));
switch (type) {
case SAVE_DATA_TYPE_PASSWORD:
case SAVE_DATA_TYPE_ADDRESS:
case SAVE_DATA_TYPE_CREDIT_CARD:
mType = type;
break;
default:
mType = SAVE_DATA_TYPE_GENERIC;
}
mRequiredIds = requiredIds;
}
/**
* @hide
* @deprecated
* // TODO(b/33197203): make sure is removed when clients migrated
*/
@Deprecated
public Builder(@SaveDataType int type) {
this(type, null);
}
/**
* @hide
* @deprecated
* // TODO(b/33197203): make sure is removed when clients migrated
*/
@Deprecated
public @NonNull Builder addSavableIds(@Nullable AutofillId... ids) {
throwIfDestroyed();
mRequiredIds = ids;
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.
*/
public @NonNull Builder setOptionalIds(@Nullable AutofillId[] ids) {
throwIfDestroyed();
if (ids != null && ids.length != 0) {
mOptionalIds = ids;
}
return this;
}
/**
* @hide
*/
// TODO(b/33197203): temporary fix to runtime crash
public @NonNull Builder addSavableIds(@Nullable AutoFillId... ids) {
throwIfDestroyed();
if (ids == null || ids.length == 0) {
return this;
}
if (mRequiredIds == null) {
mRequiredIds = new AutofillId[ids.length];
}
for (int i = 0; i < ids.length; i++) {
mRequiredIds[i] = ids[i].getDaRealId();
}
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.
*/
public @NonNull Builder setDescription(@Nullable CharSequence description) {
throwIfDestroyed();
mDescription = description;
return this;
}
/**
* Sets the title and listener for the negative save action.
*
* <p>This allows a fill-provider to customize the text and be
* notified when the user selects the negative action in the save
* UI. Note that selecting the negative action regardless of its text
* and listener being customized would dismiss the save UI and if a
* custom listener intent is provided then this intent will be
* started.</p>
*
* <p>This customization could be useful for providing additional
* semantics to the negative action. For example, a fill-provider
* can use this mechanism to add a "Disable" function or a "More info"
* function, etc. Note that the save action is exclusively controlled
* by the platform to ensure user consent is collected to release
* data from the filled app to the fill-provider.</p>
*
* @param title The action title.
* @param listener The action listener.
* @return This builder.
*
* @throws IllegalArgumentException If the title and the listener
* are not both either null or non-null.
*/
public @NonNull Builder setNegativeAction(@Nullable CharSequence title,
@Nullable IntentSender listener) {
throwIfDestroyed();
if (title == null ^ listener == null) {
throw new IllegalArgumentException("title and listener"
+ " must be both non-null or null");
}
mNegativeActionTitle = title;
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 (!DEBUG) return super.toString();
return new StringBuilder("SaveInfo: [type=").append(mType)
.append(", requiredIds=").append(Arrays.toString(mRequiredIds))
.append(", optionalIds=").append(Arrays.toString(mOptionalIds))
.append(", description=").append(mDescription)
.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.writeCharSequence(mNegativeActionTitle);
parcel.writeParcelable(mNegativeActionListener, flags);
parcel.writeParcelableArray(mOptionalIds, flags);
parcel.writeCharSequence(mDescription);
}
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 Builder builder = new Builder(parcel.readInt(),
parcel.readParcelableArray(null, AutofillId.class));
builder.setNegativeAction(parcel.readCharSequence(), parcel.readParcelable(null));
builder.setOptionalIds(parcel.readParcelableArray(null, AutofillId.class));
builder.setDescription(parcel.readCharSequence());
return builder.build();
}
@Override
public SaveInfo[] newArray(int size) {
return new SaveInfo[size];
}
};
}