diff options
17 files changed, 631 insertions, 208 deletions
diff --git a/api/test-current.txt b/api/test-current.txt index b18153885867..de0945b511da 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -457,19 +457,12 @@ package android.service.autofill { method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception; } - public final class FieldsDetection implements android.os.Parcelable { - ctor public FieldsDetection(android.view.autofill.AutofillId, java.lang.String, java.lang.String); - method public int describeContents(); - method public void writeToParcel(android.os.Parcel, int); - field public static final android.os.Parcelable.Creator<android.service.autofill.FieldsDetection> CREATOR; - } - public static final class FillEventHistory.Event { - method public java.util.Map<java.lang.String, java.lang.Integer> getDetectedFields(); + method public java.util.Map<java.lang.String, java.lang.Integer> getFieldsClassification(); } public static final class FillResponse.Builder { - method public android.service.autofill.FillResponse.Builder setFieldsDetection(android.service.autofill.FieldsDetection); + method public android.service.autofill.FillResponse.Builder setFieldClassificationIds(android.view.autofill.AutofillId...); } public final class ImageTransformation extends android.service.autofill.InternalTransformation implements android.os.Parcelable android.service.autofill.Transformation { @@ -501,6 +494,22 @@ package android.service.autofill { method public android.view.autofill.AutofillValue sanitize(android.view.autofill.AutofillValue); } + public final class UserData implements android.os.Parcelable { + method public int describeContents(); + method public static int getMaxFieldClassificationIdsSize(); + method public static int getMaxUserDataSize(); + method public static int getMaxValueLength(); + method public static int getMinValueLength(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.UserData> CREATOR; + } + + public static final class UserData.Builder { + ctor public UserData.Builder(java.lang.String, java.lang.String); + method public android.service.autofill.UserData.Builder add(java.lang.String, java.lang.String); + method public android.service.autofill.UserData build(); + } + public abstract interface ValueFinder { method public abstract java.lang.String findByAutofillId(android.view.autofill.AutofillId); } @@ -983,6 +992,11 @@ package android.view.autofill { ctor public AutofillId(int); } + public final class AutofillManager { + method public android.service.autofill.UserData getUserData(); + method public void setUserData(android.service.autofill.UserData); + } + } package android.widget { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1e0948a46198..775b822c39aa 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5333,6 +5333,42 @@ public final class Settings { public static final String AUTOFILL_FEATURE_FIELD_DETECTION = "autofill_field_detection"; /** + * Experimental autofill feature. + * + * <p>TODO(b/67867469): document (or remove) once feature is finished + * @hide + */ + public static final String AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE = + "autofill_user_data_max_user_data_size"; + + /** + * Experimental autofill feature. + * + * <p>TODO(b/67867469): document (or remove) once feature is finished + * @hide + */ + public static final String AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE = + "autofill_user_data_max_field_classification_size"; + + /** + * Experimental autofill feature. + * + * <p>TODO(b/67867469): document (or remove) once feature is finished + * @hide + */ + public static final String AUTOFILL_USER_DATA_MAX_VALUE_LENGTH = + "autofill_user_data_max_value_length"; + + /** + * Experimental autofill feature. + * + * <p>TODO(b/67867469): document (or remove) once feature is finished + * @hide + */ + public static final String AUTOFILL_USER_DATA_MIN_VALUE_LENGTH = + "autofill_user_data_min_value_length"; + + /** * @deprecated Use {@link android.provider.Settings.Global#DEVICE_PROVISIONED} instead */ @Deprecated diff --git a/core/java/android/service/autofill/AutofillService.java b/core/java/android/service/autofill/AutofillService.java index cd362c712b3f..1afa8b3eb4dd 100644 --- a/core/java/android/service/autofill/AutofillService.java +++ b/core/java/android/service/autofill/AutofillService.java @@ -440,7 +440,6 @@ import com.android.internal.os.SomeArgs; * save(username, password); * </pre> * - * * <a name="Privacy"></a> * <h3>Privacy</h3> * @@ -453,6 +452,13 @@ import com.android.internal.os.SomeArgs; * <p>Because this data could contain PII (Personally Identifiable Information, such as username or * email address), the service should only use it locally (i.e., in the app's process) for * heuristics purposes, but it should not be sent to external servers. + * + * <a name="FieldsClassification"></a> + * <h3>Metrics and fields classification</h3 + * + * <p>TODO(b/67867469): document it or remove this section; in particular, document the relationship + * between set/getUserData(), FillResponse.setFieldClassificationIds(), and + * FillEventHistory.getFieldsClassification. */ public abstract class AutofillService extends Service { private static final String TAG = "AutofillService"; diff --git a/core/java/android/service/autofill/FieldsDetection.java b/core/java/android/service/autofill/FieldsDetection.java deleted file mode 100644 index 550ecf687349..000000000000 --- a/core/java/android/service/autofill/FieldsDetection.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 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 android.annotation.TestApi; -import android.os.Parcel; -import android.os.Parcelable; -import android.view.autofill.AutofillId; - -/** - * Class by service to improve autofillable fields detection by tracking the meaning of fields - * manually edited by the user (when they match values provided by the service). - * - * TODO(b/67867469): - * - proper javadoc - * - unhide / remove testApi - * - add FieldsDetection management so service can set it just once and reference it in further - * calls to improve performance (and also API to refresh it) - * - rename to FieldsDetectionInfo or FieldClassification? (same for CTS tests) - * - add FieldsDetectionUnitTest once API is well-defined - * @hide - */ -@TestApi -public final class FieldsDetection implements Parcelable { - - private final AutofillId mFieldId; - private final String mRemoteId; - private final String mValue; - - /** - * Creates a field detection for just one field / value pair. - * - * @param fieldId autofill id of the field in the screen. - * @param remoteId id used by the service to identify the field later. - * @param value field value known to the service. - * - * TODO(b/67867469): - * - proper javadoc - * - change signature to allow more fields / values / match methods - * - might also need to use a builder, where the constructor is the id for the fieldsdetector - * - might need id for values as well - * - add @NonNull / check it / add unit tests - * - make 'value' input more generic so it can accept distance-based match and other matches - * - throw exception if field value is less than X characters (somewhere between 7-10) - * - make sure to limit total number of fields to around 10 or so - * - use AutofillValue instead of String (so it can compare dates, for example) - */ - public FieldsDetection(AutofillId fieldId, String remoteId, String value) { - mFieldId = fieldId; - mRemoteId = remoteId; - mValue = value; - } - - /** @hide */ - public AutofillId getFieldId() { - return mFieldId; - } - - /** @hide */ - public String getRemoteId() { - return mRemoteId; - } - - /** @hide */ - public String getValue() { - return mValue; - } - - ///////////////////////////////////// - // Object "contract" methods. // - ///////////////////////////////////// - @Override - public String toString() { - // Cannot disclose remoteId or value because they could contain PII - return new StringBuilder("FieldsDetection: [field=").append(mFieldId) - .append(", remoteId_length=").append(mRemoteId.length()) - .append(", value_length=").append(mValue.length()) - .append("]").toString(); - } - - ///////////////////////////////////// - // Parcelable "contract" methods. // - ///////////////////////////////////// - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeParcelable(mFieldId, flags); - parcel.writeString(mRemoteId); - parcel.writeString(mValue); - } - - public static final Parcelable.Creator<FieldsDetection> CREATOR = - new Parcelable.Creator<FieldsDetection>() { - @Override - public FieldsDetection createFromParcel(Parcel parcel) { - // TODO(b/67867469): remove comment below if it does not use a builder at the end - // 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. - return new FieldsDetection(parcel.readParcelable(null), parcel.readString(), - parcel.readString()); - } - - @Override - public FieldsDetection[] newArray(int size) { - return new FieldsDetection[size]; - } - }; -} diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java index 736d9ef48d04..eedb972e4ea6 100644 --- a/core/java/android/service/autofill/FillEventHistory.java +++ b/core/java/android/service/autofill/FillEventHistory.java @@ -58,11 +58,6 @@ import java.util.Set; */ public final class FillEventHistory implements Parcelable { /** - * Not in parcel. The UID of the {@link AutofillService} that created the {@link FillResponse}. - */ - private final int mServiceUid; - - /** * Not in parcel. The ID of the autofill session that created the {@link FillResponse}. */ private final int mSessionId; @@ -70,17 +65,6 @@ public final class FillEventHistory implements Parcelable { @Nullable private final Bundle mClientState; @Nullable List<Event> mEvents; - /** - * Gets the UID of the {@link AutofillService} that created the {@link FillResponse}. - * - * @return The UID of the {@link AutofillService} - * - * @hide - */ - public int getServiceUid() { - return mServiceUid; - } - /** @hide */ public int getSessionId() { return mSessionId; @@ -123,9 +107,8 @@ public final class FillEventHistory implements Parcelable { /** * @hide */ - public FillEventHistory(int serviceUid, int sessionId, @Nullable Bundle clientState) { + public FillEventHistory(int sessionId, @Nullable Bundle clientState) { mClientState = clientState; - mServiceUid = serviceUid; mSessionId = sessionId; } @@ -364,16 +347,17 @@ public final class FillEventHistory implements Parcelable { } /** - * Gets the results of the last {@link FieldsDetection} request. + * Gets the results of the last fields classification request. * * @return map of edit-distance match ({@code 0} means full match, - * {@code 1} means 1 character different, etc...) by remote id (as set in the - * {@link FieldsDetection} constructor), or {@code null} if none of the user-input values + * {@code 1} means 1 character different, etc...) by remote id (as set on + * {@link UserData.Builder#add(String, android.view.autofill.AutofillValue)}), + * or {@code null} if none of the user-input values * matched the requested detection. * * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the - * service requested {@link FillResponse.Builder#setFieldsDetection(FieldsDetection) fields - * detection}. + * service requested {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...) + * fields detection}. * * TODO(b/67867469): * - improve javadoc @@ -382,11 +366,12 @@ public final class FillEventHistory implements Parcelable { * - unhide * - unhide / remove testApi * - add @NonNull / check it / add unit tests + * - add link to AutofillService #FieldsClassification anchor * * @hide */ @TestApi - @NonNull public Map<String, Integer> getDetectedFields() { + @NonNull public Map<String, Integer> getFieldsClassification() { if (mDetectedRemoteId == null || mDetectedFieldScore == -1) { return Collections.emptyMap(); } @@ -534,7 +519,7 @@ public final class FillEventHistory implements Parcelable { new Parcelable.Creator<FillEventHistory>() { @Override public FillEventHistory createFromParcel(Parcel parcel) { - FillEventHistory selection = new FillEventHistory(0, 0, parcel.readBundle()); + FillEventHistory selection = new FillEventHistory(0, parcel.readBundle()); final int numEvents = parcel.readInt(); for (int i = 0; i < numEvents; i++) { diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java index 84a0974d11cd..dff40ffc3e18 100644 --- a/core/java/android/service/autofill/FillResponse.java +++ b/core/java/android/service/autofill/FillResponse.java @@ -76,7 +76,7 @@ public final class FillResponse implements Parcelable { private final @Nullable AutofillId[] mAuthenticationIds; private final @Nullable AutofillId[] mIgnoredIds; private final long mDisableDuration; - private final @Nullable FieldsDetection mFieldsDetection; + private final @Nullable AutofillId[] mFieldClassificationIds; private final int mFlags; private int mRequestId; @@ -89,7 +89,7 @@ public final class FillResponse implements Parcelable { mAuthenticationIds = builder.mAuthenticationIds; mIgnoredIds = builder.mIgnoredIds; mDisableDuration = builder.mDisableDuration; - mFieldsDetection = builder.mFieldsDetection; + mFieldClassificationIds = builder.mFieldClassificationIds; mFlags = builder.mFlags; mRequestId = INVALID_REQUEST_ID; } @@ -135,8 +135,8 @@ public final class FillResponse implements Parcelable { } /** @hide */ - public @Nullable FieldsDetection getFieldsDetection() { - return mFieldsDetection; + public @Nullable AutofillId[] getFieldClassificationIds() { + return mFieldClassificationIds; } /** @hide */ @@ -175,7 +175,7 @@ public final class FillResponse implements Parcelable { private AutofillId[] mAuthenticationIds; private AutofillId[] mIgnoredIds; private long mDisableDuration; - private FieldsDetection mFieldsDetection; + private AutofillId[] mFieldClassificationIds; private int mFlags; private boolean mDestroyed; @@ -329,21 +329,29 @@ public final class FillResponse implements Parcelable { } /** + * Sets which fields are used for <a href="#FieldsClassification">fields classification</a> + * + * @throws IllegalArgumentException is length of {@code ids} args is more than + * {@link UserData#getMaxFieldClassificationIdsSize()}. + * @throws IllegalStateException if {@link #build()} or {@link #disableAutofill(long)} was + * already called. + * @throws NullPointerException if {@code ids} or any element on it is {@code null}. + * * TODO(b/67867469): - * - javadoc it - * - javadoc how to check results - * - unhide + * - improve javadoc: explain relationship with UserData and how to check results * - unhide / remove testApi - * - throw exception (and document) if response has datasets or saveinfo - * - throw exception (and document) if id on fieldsDetection is ignored + * - implement multiple ids * * @hide */ @TestApi - public Builder setFieldsDetection(@NonNull FieldsDetection fieldsDetection) { + public Builder setFieldClassificationIds(@NonNull AutofillId... ids) { throwIfDestroyed(); throwIfDisableAutofillCalled(); - mFieldsDetection = Preconditions.checkNotNull(fieldsDetection); + Preconditions.checkArrayElementsNotNull(ids, "ids"); + Preconditions.checkArgumentInRange(ids.length, 1, + UserData.getMaxFieldClassificationIdsSize(), "ids length"); + mFieldClassificationIds = ids; return this; } @@ -391,16 +399,17 @@ public final class FillResponse implements Parcelable { * @throws IllegalArgumentException if {@code duration} is not a positive number. * @throws IllegalStateException if either {@link #addDataset(Dataset)}, * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, - * {@link #setSaveInfo(SaveInfo)}, or {@link #setClientState(Bundle)} - * was already called. + * {@link #setSaveInfo(SaveInfo)}, {@link #setClientState(Bundle)}, or + * {link #setFieldClassificationIds(AutofillId...)} was already called. */ + // TODO(b/67867469): add @ to {link setFieldClassificationIds} once it's public public Builder disableAutofill(long duration) { throwIfDestroyed(); if (duration <= 0) { throw new IllegalArgumentException("duration must be greater than 0"); } if (mAuthentication != null || mDatasets != null || mSaveInfo != null - || mFieldsDetection != null || mClientState != null) { + || mFieldClassificationIds != null || mClientState != null) { throw new IllegalStateException("disableAutofill() must be the only method called"); } @@ -417,15 +426,18 @@ public final class FillResponse implements Parcelable { * <li>No call was made to {@link #addDataset(Dataset)}, * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, * {@link #setSaveInfo(SaveInfo)}, {@link #disableAutofill(long)}, - * or {@link #setClientState(Bundle)}. + * {@link #setClientState(Bundle)}, + * or {link #setFieldClassificationIds(AutofillId...)}. * </ol> * * @return A built response. */ + // TODO(b/67867469): add @ to {link setFieldClassificationIds} once it's public public FillResponse build() { throwIfDestroyed(); if (mAuthentication == null && mDatasets == null && mSaveInfo == null - && mDisableDuration == 0 && mFieldsDetection == null && mClientState == null) { + && mDisableDuration == 0 && mFieldClassificationIds == null + && mClientState == null) { throw new IllegalStateException("need to provide: at least one DataSet, or a " + "SaveInfo, or an authentication with a presentation, " + "or a FieldsDetection, or a client state, or disable autofill"); @@ -466,7 +478,8 @@ public final class FillResponse implements Parcelable { .append(", ignoredIds=").append(Arrays.toString(mIgnoredIds)) .append(", disableDuration=").append(mDisableDuration) .append(", flags=").append(mFlags) - .append(", fieldDetection=").append(mFieldsDetection) + .append(", fieldClassificationIds=") + .append(Arrays.toString(mFieldClassificationIds)) .append("]") .toString(); } @@ -490,7 +503,7 @@ public final class FillResponse implements Parcelable { parcel.writeParcelable(mPresentation, flags); parcel.writeParcelableArray(mIgnoredIds, flags); parcel.writeLong(mDisableDuration); - parcel.writeParcelable(mFieldsDetection, flags); + parcel.writeParcelableArray(mFieldClassificationIds, flags); parcel.writeInt(mFlags); parcel.writeInt(mRequestId); } @@ -526,9 +539,10 @@ public final class FillResponse implements Parcelable { if (disableDuration > 0) { builder.disableAutofill(disableDuration); } - final FieldsDetection fieldsDetection = parcel.readParcelable(null); - if (fieldsDetection != null) { - builder.setFieldsDetection(fieldsDetection); + final AutofillId[] fieldClassifactionIds = + parcel.readParcelableArray(null, AutofillId.class); + if (fieldClassifactionIds != null) { + builder.setFieldClassificationIds(fieldClassifactionIds); } builder.setFlags(parcel.readInt()); diff --git a/core/java/android/service/autofill/UserData.aidl b/core/java/android/service/autofill/UserData.aidl new file mode 100644 index 000000000000..76016ded424a --- /dev/null +++ b/core/java/android/service/autofill/UserData.aidl @@ -0,0 +1,20 @@ +/** + * 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; + +parcelable UserData; +parcelable UserData.Constraints; diff --git a/core/java/android/service/autofill/UserData.java b/core/java/android/service/autofill/UserData.java new file mode 100644 index 000000000000..16d8d4ad3d86 --- /dev/null +++ b/core/java/android/service/autofill/UserData.java @@ -0,0 +1,288 @@ +/* + * Copyright 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.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE; +import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE; +import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH; +import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH; +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TestApi; +import android.app.ActivityThread; +import android.content.ContentResolver; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.Settings; +import android.util.ArraySet; +import android.util.Log; +import android.view.autofill.Helper; + +import com.android.internal.util.Preconditions; + +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Class used by service to improve autofillable fields detection by tracking the meaning of fields + * manually edited by the user (when they match values provided by the service). + * + * TODO(b/67867469): + * - improve javadoc / add link to section on AutofillService + * - unhide / remove testApi + * @hide + */ +@TestApi +public final class UserData implements Parcelable { + + private static final String TAG = "UserData"; + + private static final int DEFAULT_MAX_USER_DATA_SIZE = 10; + private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10; + private static final int DEFAULT_MIN_VALUE_LENGTH = 5; + private static final int DEFAULT_MAX_VALUE_LENGTH = 100; + + private final String[] mRemoteIds; + private final String[] mValues; + + private UserData(Builder builder) { + mRemoteIds = new String[builder.mRemoteIds.size()]; + builder.mRemoteIds.toArray(mRemoteIds); + mValues = new String[builder.mValues.size()]; + builder.mValues.toArray(mValues); + } + + /** @hide */ + public String[] getRemoteIds() { + return mRemoteIds; + } + + /** @hide */ + public String[] getValues() { + return mValues; + } + + /** @hide */ + public void dump(String prefix, PrintWriter pw) { + // Cannot disclose remote ids because they could contain PII + pw.print(prefix); pw.print("Remote ids size: "); pw.println(mRemoteIds.length); + for (int i = 0; i < mValues.length; i++) { + pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); pw.println(mValues[i]); + } + } + + /** @hide */ + public static void dumpConstraints(String prefix, PrintWriter pw) { + pw.print(prefix); pw.print("maxUserDataSize: "); pw.println(getMaxUserDataSize()); + pw.print(prefix); pw.print("maxFieldClassificationIdsSize: "); + pw.println(getMaxFieldClassificationIdsSize()); + pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength()); + pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength()); + } + + /** + * A builder for {@link UserData} objects. + * + * TODO(b/67867469): unhide / remove testApi + * + * @hide + */ + @TestApi + public static final class Builder { + private final ArraySet<String> mRemoteIds; + private final ArrayList<String> mValues; + private boolean mDestroyed; + + /** + * Creates a new builder for the user data used for <a href="#FieldsClassification">fields + * classification</a>. + * + * @throws IllegalArgumentException if {@code remoteId} or {@code value} are empty or if the + * length of {@code value} is lower than {@link UserData#getMinValueLength()} + * or higher than {@link UserData#getMaxValueLength()}. + */ + public Builder(@NonNull String remoteId, @NonNull String value) { + checkValidRemoteId(remoteId); + checkValidValue(value); + final int capacity = getMaxUserDataSize(); + mRemoteIds = new ArraySet<>(capacity); + mValues = new ArrayList<>(capacity); + mRemoteIds.add(remoteId); + mValues.add(value); + } + + /** + * Adds a new value for user data. + * + * @param remoteId unique string used to identify the user data. + * @param value value of the user data. + * + * @throws IllegalStateException if {@link #build()} or + * {@link #add(String, String)} with the same {@code remoteId} has already + * been called, or if the number of values add (i.e., calls made to this method plus + * constructor) is more than {@link UserData#getMaxUserDataSize()}. + * + * @throws IllegalArgumentException if {@code remoteId} or {@code value} are empty or if the + * length of {@code value} is lower than {@link UserData#getMinValueLength()} + * or higher than {@link UserData#getMaxValueLength()}. + */ + public Builder add(@NonNull String remoteId, @NonNull String value) { + throwIfDestroyed(); + checkValidRemoteId(remoteId); + checkValidValue(value); + + Preconditions.checkState(!mRemoteIds.contains(remoteId), + // Don't include remoteId on message because it could contain PII + "already has entry with same remoteId"); + Preconditions.checkState(mRemoteIds.size() < getMaxUserDataSize(), + "already added " + mRemoteIds.size() + " elements"); + mRemoteIds.add(remoteId); + mValues.add(value); + return this; + } + + private void checkValidRemoteId(@Nullable String remoteId) { + Preconditions.checkNotNull(remoteId); + Preconditions.checkArgument(!remoteId.isEmpty(), "remoteId cannot be empty"); + } + + private void checkValidValue(@Nullable String value) { + Preconditions.checkNotNull(value); + final int length = value.length(); + Preconditions.checkArgumentInRange(length, getMinValueLength(), + getMaxValueLength(), "value length (" + length + ")"); + } + + /** + * Creates a new {@link UserData} instance. + * + * <p>You should not interact with this builder once this method is called. + * + * @throws IllegalStateException if {@link #build()} was already called. + * + * @return The built dataset. + */ + public UserData build() { + throwIfDestroyed(); + mDestroyed = true; + return new UserData(this); + } + + private void throwIfDestroyed() { + if (mDestroyed) { + throw new IllegalStateException("Already called #build()"); + } + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + // Cannot disclose keys or values because they could contain PII + final StringBuilder builder = new StringBuilder("UserData: [remoteIds="); + Helper.appendRedacted(builder, mRemoteIds); + builder.append(", values="); + Helper.appendRedacted(builder, mValues); + return builder.append("]").toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeStringArray(mRemoteIds); + parcel.writeStringArray(mValues); + } + + public static final Parcelable.Creator<UserData> CREATOR = + new Parcelable.Creator<UserData>() { + @Override + public UserData 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 String[] remoteIds = parcel.readStringArray(); + final String[] values = parcel.readStringArray(); + final Builder builder = new Builder(remoteIds[0], values[0]); + for (int i = 1; i < remoteIds.length; i++) { + builder.add(remoteIds[i], values[i]); + } + return builder.build(); + } + + @Override + public UserData[] newArray(int size) { + return new UserData[size]; + } + }; + + /** + * Gets the maximum number of values that can be added to a {@link UserData}. + */ + public static int getMaxUserDataSize() { + return getInt(AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, DEFAULT_MAX_USER_DATA_SIZE); + } + + /** + * Gets the maximum number of ids that can be passed to {@link + * FillResponse.Builder#setFieldClassificationIds(android.view.autofill.AutofillId...)}. + */ + public static int getMaxFieldClassificationIdsSize() { + return getInt(AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE, + DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE); + } + + /** + * Gets the minimum length of values passed to {@link Builder#Builder(String, String)}. + */ + public static int getMinValueLength() { + return getInt(AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, DEFAULT_MIN_VALUE_LENGTH); + } + + /** + * Gets the maximum length of values passed to {@link Builder#Builder(String, String)}. + */ + public static int getMaxValueLength() { + return getInt(AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, DEFAULT_MAX_VALUE_LENGTH); + } + + private static int getInt(String settings, int defaultValue) { + ContentResolver cr = null; + final ActivityThread at = ActivityThread.currentActivityThread(); + if (at != null) { + cr = at.getApplication().getContentResolver(); + } + + if (cr == null) { + Log.w(TAG, "Could not read from " + settings + "; hardcoding " + defaultValue); + return defaultValue; + } + return Settings.Secure.getInt(cr, settings, defaultValue); + } +} diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 547e0db9e841..9a99e5398c8e 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -24,6 +24,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemService; +import android.annotation.TestApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -36,6 +37,7 @@ import android.os.Parcelable; import android.os.RemoteException; import android.service.autofill.AutofillService; import android.service.autofill.FillEventHistory; +import android.service.autofill.UserData; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -1007,6 +1009,54 @@ public final class AutofillManager { } /** + * Gets the user data used for <a href="#FieldsClassification">fields classification</a>. + * + * <p><b>Note:</b> This method should only be called by an app providing an autofill service. + * + * TODO(b/67867469): + * - proper javadoc + * - unhide / remove testApi + * + * @return value previously set by {@link #setUserData(UserData)} or {@code null} if it was + * reset or if the caller currently does not have an enabled autofill service for the user. + * + * @hide + */ + @TestApi + @Nullable public UserData getUserData() { + try { + return mService.getUserData(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return null; + } + } + + /** + * Sets the user data used for <a href="#FieldsClassification">fields classification</a>. + * + * <p><b>Note:</b> This method should only be called by an app providing an autofill service, + * and it's ignored if the caller currently doesn't have an enabled autofill service for + * the user. + * + * TODO(b/67867469): + * - proper javadoc + * - unhide / remove testApi + * - add unit tests: + * - call set / get / verify + * + * @hide + */ + @TestApi + public void setUserData(@Nullable UserData userData) { + try { + mService.setUserData(userData); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** * Returns {@code true} if autofill is supported by the current device and * is supported for this user. * diff --git a/core/java/android/view/autofill/AutofillValue.java b/core/java/android/view/autofill/AutofillValue.java index 3beae11cf38c..8e649de52c97 100644 --- a/core/java/android/view/autofill/AutofillValue.java +++ b/core/java/android/view/autofill/AutofillValue.java @@ -177,7 +177,7 @@ public final class AutofillValue implements Parcelable { .append("[type=").append(mType) .append(", value="); if (isText()) { - string.append(((CharSequence) mValue).length()).append("_chars"); + Helper.appendRedacted(string, (CharSequence) mValue); } else { string.append(mValue); } diff --git a/core/java/android/view/autofill/Helper.java b/core/java/android/view/autofill/Helper.java index 829e7f3aa5ac..b95704af7d44 100644 --- a/core/java/android/view/autofill/Helper.java +++ b/core/java/android/view/autofill/Helper.java @@ -16,6 +16,8 @@ package android.view.autofill; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.os.Bundle; import java.util.Arrays; @@ -50,6 +52,35 @@ public final class Helper { return builder; } + /** + * Appends {@code value} to the {@code builder} redacting its contents. + */ + public static void appendRedacted(@NonNull StringBuilder builder, + @Nullable CharSequence value) { + if (value == null) { + builder.append("null"); + } else { + builder.append(value.length()).append("_chars"); + } + } + + /** + * Appends {@code values} to the {@code builder} redacting its contents. + */ + public static void appendRedacted(@NonNull StringBuilder builder, @Nullable String[] values) { + if (values == null) { + builder.append("N/A"); + return; + } + builder.append("["); + for (String value : values) { + builder.append(" '"); + appendRedacted(builder, value); + builder.append("'"); + } + builder.append(" ]"); + } + private Helper() { throw new UnsupportedOperationException("contains static members only"); } diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl index d6db3fe573f5..7d6a19f529ce 100644 --- a/core/java/android/view/autofill/IAutoFillManager.aidl +++ b/core/java/android/view/autofill/IAutoFillManager.aidl @@ -21,6 +21,7 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; import android.service.autofill.FillEventHistory; +import android.service.autofill.UserData; import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; import android.view.autofill.IAutoFillManagerClient; @@ -53,4 +54,6 @@ interface IAutoFillManager { boolean isServiceSupported(int userId); boolean isServiceEnabled(int userId, String packageName); void onPendingSaveUi(int operation, IBinder token); + UserData getUserData(); + void setUserData(in UserData userData); } diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java index 4ce60294e615..2e1c0a1d1fde 100644 --- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java +++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java @@ -419,7 +419,13 @@ public class SettingsBackupTest { private static final Set<String> BACKUP_BLACKLISTED_SECURE_SETTINGS = newHashSet( Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, + // TODO(b/67867469): Move autofill settings below to + // BACKUP_BLACKLISTED_SYSTEM_SETTINGS once feature is moved out of experimental Settings.Secure.AUTOFILL_FEATURE_FIELD_DETECTION, + Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE, + Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, + Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, + Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, Settings.Secure.ALLOWED_GEOLOCATION_ORIGINS, Settings.Secure.ALWAYS_ON_VPN_APP, Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN, diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java index 23e4f504735b..02912763509f 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java @@ -52,6 +52,7 @@ import android.os.UserManager; import android.os.UserManagerInternal; import android.provider.Settings; import android.service.autofill.FillEventHistory; +import android.service.autofill.UserData; import android.util.LocalLog; import android.util.Slog; import android.util.SparseArray; @@ -581,6 +582,34 @@ public final class AutofillManagerService extends SystemService { } @Override + public UserData getUserData() throws RemoteException { + UserHandle user = getCallingUserHandle(); + int uid = getCallingUid(); + + synchronized (mLock) { + AutofillManagerServiceImpl service = peekServiceForUserLocked(user.getIdentifier()); + if (service != null) { + return service.getUserData(uid); + } + } + + return null; + } + + @Override + public void setUserData(UserData userData) throws RemoteException { + UserHandle user = getCallingUserHandle(); + int uid = getCallingUid(); + + synchronized (mLock) { + AutofillManagerServiceImpl service = peekServiceForUserLocked(user.getIdentifier()); + if (service != null) { + service.setUserData(uid, userData); + } + } + } + + @Override public boolean restoreSession(int sessionId, IBinder activityToken, IBinder appCallback) throws RemoteException { activityToken = Preconditions.checkNotNull(activityToken, "activityToken"); @@ -723,6 +752,7 @@ public final class AutofillManagerService extends SystemService { } boolean oldDebug = sDebug; + final String prefix = " "; try { synchronized (mLock) { oldDebug = sDebug; @@ -731,6 +761,7 @@ public final class AutofillManagerService extends SystemService { pw.print("Verbose mode: "); pw.println(sVerbose); pw.print("Disabled users: "); pw.println(mDisabledUsers); pw.print("Max partitions per session: "); pw.println(sPartitionMaxCount); + pw.println("User data constraints: "); UserData.dumpConstraints(prefix, pw); final int size = mServicesCache.size(); pw.print("Cached services: "); if (size == 0) { @@ -740,7 +771,7 @@ public final class AutofillManagerService extends SystemService { for (int i = 0; i < size; i++) { pw.print("\nService at index "); pw.println(i); final AutofillManagerServiceImpl impl = mServicesCache.valueAt(i); - impl.dumpLocked(" ", pw); + impl.dumpLocked(prefix, pw); } } mUi.dump(pw); diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 21e27220b621..8b6dc2028b91 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -51,6 +51,7 @@ import android.service.autofill.FillEventHistory; import android.service.autofill.FillEventHistory.Event; import android.service.autofill.FillResponse; import android.service.autofill.IAutoFillService; +import android.service.autofill.UserData; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -121,6 +122,11 @@ final class AutofillManagerServiceImpl { private boolean mDisabled; /** + * Data used for field classification. + */ + private UserData mUserData; + + /** * Caches whether the setup completed for the current user. */ @GuardedBy("mLock") @@ -183,6 +189,14 @@ final class AutofillManagerServiceImpl { } } + private int getServiceUidLocked() { + if (mInfo == null) { + Slog.w(TAG, "getServiceUidLocked(): no mInfo"); + return -1; + } + return mInfo.getServiceInfo().applicationInfo.uid; + } + @Nullable String getServicePackageName() { final ComponentName serviceComponent = getServiceComponentName(); @@ -574,9 +588,9 @@ final class AutofillManagerServiceImpl { * Initializes the last fill selection after an autofill service returned a new * {@link FillResponse}. */ - void setLastResponse(int serviceUid, int sessionId, @NonNull FillResponse response) { + void setLastResponse(int sessionId, @NonNull FillResponse response) { synchronized (mLock) { - mEventHistory = new FillEventHistory(serviceUid, sessionId, response.getClientState()); + mEventHistory = new FillEventHistory(sessionId, response.getClientState()); } } @@ -688,18 +702,54 @@ final class AutofillManagerServiceImpl { */ FillEventHistory getFillEventHistory(int callingUid) { synchronized (mLock) { - if (mEventHistory != null && mEventHistory.getServiceUid() == callingUid) { + if (mEventHistory != null + && isCalledByServiceLocked("getFillEventHistory", callingUid)) { return mEventHistory; } } + return null; + } + // Called by Session - does not need to check uid + UserData getUserData() { + synchronized (mLock) { + return mUserData; + } + } + + // Called by AutofillManager + UserData getUserData(int callingUid) { + synchronized (mLock) { + if (isCalledByServiceLocked("getUserData", callingUid)) { + return mUserData; + } + } return null; } + // Called by AutofillManager + void setUserData(int callingUid, UserData userData) { + synchronized (mLock) { + if (isCalledByServiceLocked("setUserData", callingUid)) { + mUserData = userData; + } + } + } + + private boolean isCalledByServiceLocked(String methodName, int callingUid) { + if (getServiceUidLocked() != callingUid) { + Slog.w(TAG, methodName + "() called by UID " + callingUid + + ", but service UID is " + getServiceUidLocked()); + return false; + } + return true; + } + void dumpLocked(String prefix, PrintWriter pw) { final String prefix2 = prefix + " "; pw.print(prefix); pw.print("User: "); pw.println(mUserId); + pw.print(prefix); pw.print("UID: "); pw.println(getServiceUidLocked()); pw.print(prefix); pw.print("Component: "); pw.println(mInfo != null ? mInfo.getServiceInfo().getComponentName() : null); pw.print(prefix); pw.print("Component from settings: "); @@ -762,8 +812,13 @@ final class AutofillManagerServiceImpl { } } - pw.print(prefix); pw.println("Clients"); - mClients.dump(pw, prefix2); + pw.print(prefix); pw.print("Clients: "); + if (mClients == null) { + pw.println("N/A"); + } else { + pw.println(); + mClients.dump(pw, prefix2); + } if (mEventHistory == null || mEventHistory.getEvents() == null || mEventHistory.getEvents().size() == 0) { @@ -779,6 +834,14 @@ final class AutofillManagerServiceImpl { + event.getDatasetId()); } } + + pw.print(prefix); pw.print("User data: "); + if (mUserData == null) { + pw.println("N/A"); + } else { + pw.println(); + mUserData.dump(prefix2, pw); + } } void destroySessionsLocked() { diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java index 831c488e95b2..aea9ad0e33da 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java @@ -97,7 +97,7 @@ final class RemoteFillService implements DeathRecipient { private PendingRequest mPendingRequest; public interface FillServiceCallbacks { - void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response, int serviceUid, + void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response, @NonNull String servicePackageName); void onFillRequestFailure(@Nullable CharSequence message, @NonNull String servicePackageName); @@ -281,11 +281,11 @@ final class RemoteFillService implements DeathRecipient { mContext.unbindService(mServiceConnection); } - private void dispatchOnFillRequestSuccess(PendingRequest pendingRequest, - int callingUid, int requestFlags, FillResponse response) { + private void dispatchOnFillRequestSuccess(PendingRequest pendingRequest, int requestFlags, + FillResponse response) { mHandler.getHandler().post(() -> { if (handleResponseCallbackCommon(pendingRequest)) { - mCallbacks.onFillRequestSuccess(requestFlags, response, callingUid, + mCallbacks.onFillRequestSuccess(requestFlags, response, mComponentName.getPackageName()); } }); @@ -546,7 +546,7 @@ final class RemoteFillService implements DeathRecipient { final RemoteFillService remoteService = getService(); if (remoteService != null) { remoteService.dispatchOnFillRequestSuccess(PendingFillRequest.this, - getCallingUid(), request.getFlags(), response); + request.getFlags(), response); } } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 99b92b9c9cd4..4a054f734738 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -55,7 +55,6 @@ import android.os.RemoteException; import android.os.SystemClock; import android.service.autofill.AutofillService; import android.service.autofill.Dataset; -import android.service.autofill.FieldsDetection; import android.service.autofill.FillContext; import android.service.autofill.FillRequest; import android.service.autofill.FillResponse; @@ -63,6 +62,7 @@ import android.service.autofill.InternalSanitizer; import android.service.autofill.InternalValidator; import android.service.autofill.SaveInfo; import android.service.autofill.SaveRequest; +import android.service.autofill.UserData; import android.service.autofill.ValueFinder; import android.util.ArrayMap; import android.util.ArraySet; @@ -480,7 +480,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // FillServiceCallbacks @Override public void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response, - int serviceUid, @NonNull String servicePackageName) { + @NonNull String servicePackageName) { synchronized (mLock) { if (mDestroyed) { Slog.w(TAG, "Call to Session#onFillRequestSuccess() rejected - session: " @@ -494,13 +494,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } // TODO(b/67867469): remove once feature is finished - if (response.getFieldsDetection() != null && !mService.isFieldDetectionEnabled()) { + if (response.getFieldClassificationIds() != null && !mService.isFieldDetectionEnabled()) { Slog.w(TAG, "Ignoring " + response + " because field detection is disabled"); processNullResponseLocked(requestFlags); return; } - mService.setLastResponse(serviceUid, id, response); + mService.setLastResponse(id, response); int sessionFinishedState = 0; final long disableDuration = response.getDisableDuration(); @@ -903,7 +903,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final FillResponse response = mResponses.valueAt(i); final List<Dataset> datasets = response.getDatasets(); if (datasets == null || datasets.isEmpty()) { - if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i); + if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i); } else { for (int j = 0; j < datasets.size(); j++) { final Dataset dataset = datasets.get(j); @@ -926,25 +926,27 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } } - final FieldsDetection fieldsDetection = lastResponse.getFieldsDetection(); + final AutofillId[] fieldClassificationIds = lastResponse.getFieldClassificationIds(); - if (!hasAtLeastOneDataset && fieldsDetection == null) { + if (!hasAtLeastOneDataset && fieldClassificationIds == null) { if (sVerbose) { Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets nor fields " - + "detection)"); + + "classification ids)"); } return; } + final UserData userData = mService.getUserData(); final AutofillId detectableFieldId; final String detectableRemoteId; String detectedRemoteId = null; - if (fieldsDetection == null) { + if (userData == null) { detectableFieldId = null; detectableRemoteId = null; } else { - detectableFieldId = fieldsDetection.getFieldId(); - detectableRemoteId = fieldsDetection.getRemoteId(); + // TODO(b/67867469): hardcoded to just first entry on initial refactoring. + detectableFieldId = fieldClassificationIds[0]; + detectableRemoteId = userData.getRemoteIds()[0]; } int detectedFieldScore = -1; @@ -1057,7 +1059,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (detectableFieldId != null && detectableFieldId.equals(viewState.id) && currentValue.isText() && currentValue.getTextValue() != null) { final String actualValue = currentValue.getTextValue().toString(); - final String expectedValue = fieldsDetection.getValue(); + // TODO(b/67867469): hardcoded to just first entry on initial refactoring. + final String expectedValue = userData.getValues()[0]; if (actualValue.equalsIgnoreCase(expectedValue)) { detectedRemoteId = detectableRemoteId; detectedFieldScore = 0; |