diff options
| author | TreeHugger Robot <treehugger-gerrit@google.com> | 2017-12-04 19:46:51 +0000 |
|---|---|---|
| committer | Android (Google) Code Review <android-gerrit@google.com> | 2017-12-04 19:46:51 +0000 |
| commit | 3ce82e34e6684951ba89e2bfb49e8ef3ca8c951d (patch) | |
| tree | 49fdea236eb499397833079edaf37de0d4e3e152 /core/java | |
| parent | a18d357251494f822b7a2edff443f3a7da947786 (diff) | |
| parent | 452886a5b8d7cb94ba0c53e8976ff558980db1e5 (diff) | |
Merge "Refactored field detection mechanism to support multiple fields."
Diffstat (limited to 'core/java')
| -rw-r--r-- | core/java/android/provider/Settings.java | 36 | ||||
| -rw-r--r-- | core/java/android/service/autofill/AutofillService.java | 8 | ||||
| -rw-r--r-- | core/java/android/service/autofill/FieldsDetection.java | 127 | ||||
| -rw-r--r-- | core/java/android/service/autofill/FillEventHistory.java | 35 | ||||
| -rw-r--r-- | core/java/android/service/autofill/FillResponse.java | 58 | ||||
| -rw-r--r-- | core/java/android/service/autofill/UserData.aidl | 20 | ||||
| -rw-r--r-- | core/java/android/service/autofill/UserData.java | 288 | ||||
| -rw-r--r-- | core/java/android/view/autofill/AutofillManager.java | 50 | ||||
| -rw-r--r-- | core/java/android/view/autofill/AutofillValue.java | 2 | ||||
| -rw-r--r-- | core/java/android/view/autofill/Helper.java | 31 | ||||
| -rw-r--r-- | core/java/android/view/autofill/IAutoFillManager.aidl | 3 |
11 files changed, 482 insertions, 176 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c4c679885c09..53832c7ddf33 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); } |
