blob: 48dbb8415ec474c8ea8f782567147e3eb4799007 [file] [log] [blame]
/*
* Copyright (C) 2016 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.view.autofill;
import static android.view.autofill.Helper.DEBUG;
import static android.view.autofill.Helper.append;
import android.annotation.Nullable;
import android.app.Activity;
import android.hardware.fingerprint.FingerprintManager.CryptoObject;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.service.autofill.FillCallback;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Response for a {@link
* android.service.autofill.AutoFillService#onFillRequest(android.app.assist.AssistStructure,
* Bundle, android.os.CancellationSignal, android.service.autofill.FillCallback)} request.
*
* <p>The response typically contains one or more {@link Dataset}s, each representing a set of
* fields that can be auto-filled together, and the Android System displays a dataset picker UI
* affordance that the user must use before the {@link Activity} is filled with the dataset.
*
* <p>For example, for a login page with username/password where the user only has one account in
* the service, the response could be:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* .add(new Dataset.Builder("homer")
* .setTextFieldValue(id1, "homer")
* .setTextFieldValue(id2, "D'OH!")
* .build())
* .build();
* </pre>
*
* <p>If the user had 2 accounts, each with its own user-provided names, the response could be:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* .add(new Dataset.Builder("Homer's Account")
* .setTextFieldValue(id1, "homer")
* .setTextFieldValue(id2, "D'OH!")
* .build())
* .add(new Dataset.Builder("Bart's Account")
* .setTextFieldValue(id1, "elbarto")
* .setTextFieldValue(id2, "cowabonga")
* .build())
* .build();
* </pre>
*
* <p>If the user does not have any data associated with this {@link Activity} but the service wants
* to offer the user the option to save the data that was entered, then the service could populate
* the response with {@code savableIds} instead of {@link Dataset}s:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* .addSavableFields(id1, id2)
* .build();
* </pre>
*
* <p>Similarly, there might be cases where the user data on the service 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 populate the response with both {@link Dataset}s and {@code
* savableIds}:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* .add(new Dataset.Builder("Homer")
* .setTextFieldValue(id1, "Homer") // first name
* .setTextFieldValue(id2, "Simpson") // last name
* .setTextFieldValue(id3, "742 Evergreen Terrace") // street
* .setTextFieldValue(id4, "Springfield") // city
* .build())
* .addSavableFields(id5, id6) // state and zipcode
* .build();
*
* </pre>
*
* <p>Notice that the ids that are part of a dataset (ids 1 to 4, in this example) are automatically
* added to the {@code savableIds} list.
*
* <p>If the service has multiple {@link Dataset}s with multiple options for some fields on each
* dataset (for example, multiple accounts with both a home and work address), then it should
* "partition" the {@link Activity} in sections and populate the response with just a subset of the
* data that would fulfill the first section; then once the user fills the first section and taps a
* field from the next section, the Android system would issue another request for that section, and
* so on. For example, the first response could be:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* .add(new Dataset.Builder("Homer")
* .setTextFieldValue(id1, "Homer")
* .setTextFieldValue(id2, "Simpson")
* .build())
* .add(new Dataset.Builder("Bart")
* .setTextFieldValue(id1, "Bart")
* .setTextFieldValue(id2, "Simpson")
* .build())
* .build();
* </pre>
*
* <p>Then after the user picks the {@code Homer} dataset and taps the {@code Street} field to
* trigger another auto-fill request, the second response could be:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* .add(new Dataset.Builder("Home")
* .setTextFieldValue(id3, "742 Evergreen Terrace")
* .setTextFieldValue(id4, "Springfield")
* .build())
* .add(new Dataset.Builder("Work")
* .setTextFieldValue(id3, "Springfield Nuclear Power Plant")
* .setTextFieldValue(id4, "Springfield")
* .build())
* .build();
* </pre>
*
* <p>The service could require user authentication, either at the {@link FillResponse} or {@link
* Dataset} levels, prior to auto-filling the activity - see {@link
* FillResponse.Builder#requiresFingerprintAuthentication(CryptoObject, Bundle, int)}, {@link
* FillResponse.Builder#requiresCustomAuthentication(Bundle, int)}, {@link
* Dataset.Builder#requiresFingerprintAuthentication(CryptoObject, Bundle, int)}, and {@link
* Dataset.Builder#requiresCustomAuthentication(Bundle, int)} for details.
*
* <p>Finally, the service can use the {@link FillResponse.Builder#setExtras(Bundle)} and/or {@link
* Dataset.Builder#setExtras(Bundle)} methods to pass {@link Bundle}s with service-specific data use
* to identify this response on future calls (like {@link
* android.service.autofill.AutoFillService#onSaveRequest(android.app.assist.AssistStructure,
* Bundle, android.service.autofill.SaveCallback)}) - such bundles will be available as the
* {@link android.service.autofill.AutoFillService#EXTRA_RESPONSE_EXTRAS} and
* {@link android.service.autofill.AutoFillService#EXTRA_DATASET_EXTRAS} extras in that method's
* {@code extras} argument.
*/
public final class FillResponse implements Parcelable {
private final List<Dataset> mDatasets;
private final AutoFillId[] mSavableIds;
private final Bundle mExtras;
private final int mFlags;
private final boolean mRequiresAuth;
private final boolean mHasCryptoObject;
private final long mCryptoOpId;
private FillResponse(Builder builder) {
// TODO(b/33197203): make it immutable?
mDatasets = builder.mDatasets;
final int size = builder.mSavableIds.size();
mSavableIds = new AutoFillId[size];
int i = 0;
for (AutoFillId id : builder.mSavableIds) {
mSavableIds[i++] = id;
}
mExtras = builder.mExtras;
mFlags = builder.mFlags;
mRequiresAuth = builder.mRequiresAuth;
mHasCryptoObject = builder.mHasCryptoObject;
mCryptoOpId = builder.mCryptoOpId;
}
/** @hide */
public List<Dataset> getDatasets() {
return mDatasets;
}
/** @hide */
public AutoFillId[] getSavableIds() {
return mSavableIds;
}
/** @hide */
public Bundle getExtras() {
return mExtras;
}
/** @hide */
public int getFlags() {
return mFlags;
}
/** @hide */
public boolean isAuthRequired() {
return mRequiresAuth;
}
/** @hide */
public boolean hasCryptoObject() {
return mHasCryptoObject;
}
/** @hide */
public long getCryptoObjectOpId() {
return mCryptoOpId;
}
/**
* Builder for {@link FillResponse} objects.
*/
public static final class Builder {
private final List<Dataset> mDatasets = new ArrayList<>();
private final Set<AutoFillId> mSavableIds = new HashSet<>();
private Bundle mExtras;
private int mFlags;
private boolean mRequiresAuth;
private boolean mHasCryptoObject;
private long mCryptoOpId;
/**
* Requires user authentication through the {@link android.service.autofill.AutoFillService}
* before handling an auto-fill request.
*
* <p>This method is typically called when the device (or the service) does not support
* fingerprint authentication (and hence it cannot use {@link
* #requiresFingerprintAuthentication(CryptoObject, Bundle, int)}) or when the service needs
* to use a custom authentication UI and is used in 2 scenarios:
*
* <ol>
* <li>When the user data is encrypted and the service must authenticate an object that
* will be used to decrypt it.
* <li>When the service already acquired the user data but wants to confirm the user's
* identity before the activity is filled with it.
* </ol>
*
* <p>When this method is called, the Android System displays an UI affordance asking the
* user to tap it to auto-fill the activity; if the user taps it, the Android System calls
* {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} passing {@link
* android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_REQUESTED} in the flags and
* the same {@code extras} passed to this method. The service can then displays its custom
* authentication UI, and then call the proper method on {@link FillCallback} depending on
* the authentication result and whether this response already contains the {@link Dataset}s
* need to auto-fill the activity:
*
* <ul>
* <li>If authentication failed, call {@link
* FillCallback#onFillResponseAuthentication(int)} passing {@link
* android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} in the flags.
* <li>If authentication succeeded and this response is empty (no datasets), call {@link
* FillCallback#onSuccess(FillResponse)} with a new dataset (that does not require
* authentication).
* <li>If authentication succeeded and this response is not empty, call {@link
* FillCallback#onFillResponseAuthentication(int)} passing {@link
* android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS} in the flags.
* </ul>
*
* @param extras when set, will be passed back in the {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} call so it could be used by the service to handle state.
* @param flags optional parameters, currently ignored.
*/
public Builder requiresCustomAuthentication(@Nullable Bundle extras, int flags) {
return requiresAuthentication(null, extras, flags);
}
/**
* Requires user authentication through the Fingerprint sensor before handling an auto-fill
* request.
*
* <p>The {@link android.service.autofill.AutoFillService} typically uses this method in 2
* situations:
*
* <ol>
* <li>When the user data is encrypted and the service must authenticate an object that
* will be used to decrypt it.
* <li>When the service already acquired the user data but wants to confirm the user's
* identity before the activity is filled with it.
* </ol>
*
* <p>When this method is called, the Android System displays an UI affordance asking the
* user to use the fingerprint sensor to auto-fill the activity, and what happens after a
* successful fingerprint authentication depends on the number of {@link Dataset}s included
* in this response:
*
* <ul>
* <li>If it's empty (scenario #1 above), the Android System will call {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} passing {@link
* android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}} in the
* flags.
* <li>If it contains one dataset, the activity will be auto-filled right away.
* <li>If it contains many datasets, the Android System will show dataset picker UI, and
* then auto-fill the activity once the user select the proper datased.
* </ul>
*
* <p>If the fingerprint authentication fails, the Android System will call {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} passing {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR}
* in the flags.
*
* <p><strong>NOTE: </note> the {@link android.service.autofill.AutoFillService} should use
* the {@link android.hardware.fingerprint.FingerprintManager} to check if fingerpint
* authentication is available before using this method, and use other alternatives (such as
* {@link #requiresCustomAuthentication(Bundle, int)}) if it is not: if this method is
* called when fingerprint is not available, Android System will call {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} passing {@link
* android.service.autofill.AutoFillService#FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE}
* in the flags, but it would be wasting system resources (and worsening the user
* experience) in the process.
*
* @param crypto object that will be authenticated.
* @param extras when set, will be passed back in the {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} call so it could be used by the service to handle state.
* @param flags optional parameters, currently ignored.
*/
public Builder requiresFingerprintAuthentication(CryptoObject crypto,
@Nullable Bundle extras, int flags) {
// TODO(b/33197203): should we allow crypto to be null?
Preconditions.checkArgument(crypto != null, "must pass a CryptoObject");
return requiresAuthentication(crypto, extras, flags);
}
private Builder requiresAuthentication(CryptoObject cryptoObject, Bundle extras,
int flags) {
// There can be only one!
Preconditions.checkState(!mRequiresAuth,
"requires-authentication methods already called");
// TODO(b/33197203): make sure that either this method or setExtras() is called, but
// not both
mExtras = extras;
mFlags = flags;
mRequiresAuth = true;
if (cryptoObject != null) {
mHasCryptoObject = true;
mCryptoOpId = cryptoObject.getOpId();
}
return this;
}
/**
* Adds a new {@link Dataset} to this response.
*
* @throws IllegalArgumentException if a dataset with same {@code name} already exists.
*/
public Builder addDataset(Dataset dataset) {
Preconditions.checkNotNull(dataset, "dataset cannot be null");
// TODO(b/33197203): check if name already exists
mDatasets.add(dataset);
for (DatasetField field : dataset.getFields()) {
mSavableIds.add(field.getId());
}
return this;
}
/**
* Adds ids of additional fields that the service would be interested to save (through
* {@link android.service.autofill.AutoFillService#onSaveRequest(
* android.app.assist.AssistStructure, Bundle, android.service.autofill.SaveCallback)})
* but were not indirectly set through {@link #addDataset(Dataset)}.
*
* <p>See {@link FillResponse} for examples.
*/
public Builder addSavableFields(AutoFillId... ids) {
for (AutoFillId id : ids) {
mSavableIds.add(id);
}
return this;
}
/**
* Sets a {@link Bundle} that will be passed to subsequent calls to {@link
* android.service.autofill.AutoFillService} methods such as {@link
* android.service.autofill.AutoFillService#onSaveRequest(
* android.app.assist.AssistStructure, Bundle, android.service.autofill.SaveCallback)},
* using {@link
* android.service.autofill.AutoFillService#EXTRA_RESPONSE_EXTRAS} as the key.
*
* <p>It can be used when to keep service state in between calls.
*/
public Builder setExtras(Bundle extras) {
// TODO(b/33197203): make sure that either this method or the requires-Authentication
// ones are called, but not both
mExtras = Objects.requireNonNull(extras, "extras cannot be null");
return this;
}
/**
* Builds a new {@link FillResponse} instance.
*/
public FillResponse build() {
return new FillResponse(this);
}
}
/////////////////////////////////////
// Object "contract" methods. //
/////////////////////////////////////
@Override
public String toString() {
if (!DEBUG) return super.toString();
final StringBuilder builder = new StringBuilder("FillResponse: [datasets=")
.append(mDatasets).append(", savableIds=").append(Arrays.toString(mSavableIds))
.append(", extras=");
append(builder, mExtras)
.append(", flags=").append(mFlags)
.append(", requiresAuth: ").append(mRequiresAuth)
.append(", hasCrypto: ").append(mHasCryptoObject);
return builder.append(']').toString();
}
/////////////////////////////////////
// Parcelable "contract" methods. //
/////////////////////////////////////
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeList(mDatasets);
parcel.writeParcelableArray(mSavableIds, 0);
parcel.writeBundle(mExtras);
parcel.writeInt(mFlags);
parcel.writeInt(mRequiresAuth ? 1 : 0);
parcel.writeInt(mHasCryptoObject ? 1 : 0);
if (mHasCryptoObject) {
parcel.writeLong(mCryptoOpId);
}
}
private FillResponse(Parcel parcel) {
mDatasets = new ArrayList<>();
parcel.readList(mDatasets, null);
mSavableIds = parcel.readParcelableArray(null, AutoFillId.class);
mExtras = parcel.readBundle();
mFlags = parcel.readInt();
mRequiresAuth = parcel.readInt() == 1;
mHasCryptoObject = parcel.readInt() == 1;
mCryptoOpId = mHasCryptoObject ? parcel.readLong() : 0;
}
public static final Parcelable.Creator<FillResponse> CREATOR =
new Parcelable.Creator<FillResponse>() {
@Override
public FillResponse createFromParcel(Parcel source) {
return new FillResponse(source);
}
@Override
public FillResponse[] newArray(int size) {
return new FillResponse[size];
}
};
}