New autofill APIs: DateTransformation and DateValueSanitizer.
These APIs are useful when an app uses a date value for a credit card
expiration date.
Test: atest CtsAutoFillServiceTestCases:DateValueSanitizerTest \
CtsAutoFillServiceTestCases:DateTransformationTest \
CtsAutoFillServiceTestCases:CustomDescriptionDateTest
Fixes: 72450441
Change-Id: Ie17ab17aa07e0401f4dbba3faa80cc2cc2e7d783
diff --git a/api/current.txt b/api/current.txt
index 7185c99..58eb562 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -11606,15 +11606,15 @@
public final class AssetManager implements java.lang.AutoCloseable {
method public void close();
- method public java.lang.String[] getLocales();
- method public java.lang.String[] list(java.lang.String) throws java.io.IOException;
- method public java.io.InputStream open(java.lang.String) throws java.io.IOException;
- method public java.io.InputStream open(java.lang.String, int) throws java.io.IOException;
- method public android.content.res.AssetFileDescriptor openFd(java.lang.String) throws java.io.IOException;
- method public android.content.res.AssetFileDescriptor openNonAssetFd(java.lang.String) throws java.io.IOException;
- method public android.content.res.AssetFileDescriptor openNonAssetFd(int, java.lang.String) throws java.io.IOException;
- method public android.content.res.XmlResourceParser openXmlResourceParser(java.lang.String) throws java.io.IOException;
- method public android.content.res.XmlResourceParser openXmlResourceParser(int, java.lang.String) throws java.io.IOException;
+ method public final java.lang.String[] getLocales();
+ method public final java.lang.String[] list(java.lang.String) throws java.io.IOException;
+ method public final java.io.InputStream open(java.lang.String) throws java.io.IOException;
+ method public final java.io.InputStream open(java.lang.String, int) throws java.io.IOException;
+ method public final android.content.res.AssetFileDescriptor openFd(java.lang.String) throws java.io.IOException;
+ method public final android.content.res.AssetFileDescriptor openNonAssetFd(java.lang.String) throws java.io.IOException;
+ method public final android.content.res.AssetFileDescriptor openNonAssetFd(int, java.lang.String) throws java.io.IOException;
+ method public final android.content.res.XmlResourceParser openXmlResourceParser(java.lang.String) throws java.io.IOException;
+ method public final android.content.res.XmlResourceParser openXmlResourceParser(int, java.lang.String) throws java.io.IOException;
field public static final int ACCESS_BUFFER = 3; // 0x3
field public static final int ACCESS_RANDOM = 1; // 0x1
field public static final int ACCESS_STREAMING = 2; // 0x2
@@ -38528,6 +38528,20 @@
method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, java.util.regex.Pattern, android.widget.RemoteViews);
}
+ public final class DateTransformation implements android.os.Parcelable android.service.autofill.Transformation {
+ ctor public DateTransformation(android.view.autofill.AutofillId, java.text.DateFormat);
+ method public int describeContents();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.service.autofill.DateTransformation> CREATOR;
+ }
+
+ public final class DateValueSanitizer implements android.os.Parcelable android.service.autofill.Sanitizer {
+ ctor public DateValueSanitizer(java.text.DateFormat);
+ method public int describeContents();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.service.autofill.DateValueSanitizer> CREATOR;
+ }
+
public final class FieldClassification {
method public java.util.List<android.service.autofill.FieldClassification.Match> getMatches();
}
diff --git a/api/test-current.txt b/api/test-current.txt
index 4e8f904..d834cf7 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -565,6 +565,14 @@
method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception;
}
+ public final class DateTransformation extends android.service.autofill.InternalTransformation implements android.os.Parcelable android.service.autofill.Transformation {
+ method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception;
+ }
+
+ public final class DateValueSanitizer extends android.service.autofill.InternalSanitizer implements android.os.Parcelable android.service.autofill.Sanitizer {
+ method public android.view.autofill.AutofillValue sanitize(android.view.autofill.AutofillValue);
+ }
+
public final class FillResponse implements android.os.Parcelable {
method public int getFlags();
}
@@ -599,7 +607,8 @@
}
public abstract interface ValueFinder {
- method public abstract java.lang.String findByAutofillId(android.view.autofill.AutofillId);
+ method public default java.lang.String findByAutofillId(android.view.autofill.AutofillId);
+ method public abstract android.view.autofill.AutofillValue findRawValueByAutofillId(android.view.autofill.AutofillId);
}
}
diff --git a/core/java/android/service/autofill/DateTransformation.java b/core/java/android/service/autofill/DateTransformation.java
new file mode 100644
index 0000000..4e1425d
--- /dev/null
+++ b/core/java/android/service/autofill/DateTransformation.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.util.Preconditions;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * Replaces a {@link TextView} child of a {@link CustomDescription} with the contents of a field
+ * that is expected to have a {@link AutofillValue#forDate(long) date value}.
+ *
+ * <p>For example, a transformation to display a credit card expiration date as month/year would be:
+ *
+ * <pre class="prettyprint">
+ * new DateTransformation(ccExpDate, new java.text.SimpleDateFormat("MM/yyyy")
+ * </pre>
+ */
+public final class DateTransformation extends InternalTransformation implements
+ Transformation, Parcelable {
+ private static final String TAG = "DateTransformation";
+
+ private final AutofillId mFieldId;
+ private final DateFormat mDateFormat;
+
+ /**
+ * Creates a new transformation.
+ *
+ * @param id id of the screen field.
+ * @param dateFormat object used to transform the date value of the field to a String.
+ */
+ public DateTransformation(@NonNull AutofillId id, @NonNull DateFormat dateFormat) {
+ mFieldId = Preconditions.checkNotNull(id);
+ mDateFormat = Preconditions.checkNotNull(dateFormat);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
+ int childViewId) throws Exception {
+ final AutofillValue value = finder.findRawValueByAutofillId(mFieldId);
+ if (value == null) {
+ Log.w(TAG, "No value for id " + mFieldId);
+ return;
+ }
+ if (!value.isDate()) {
+ Log.w(TAG, "Value for " + mFieldId + " is not date: " + value);
+ return;
+ }
+
+ try {
+ final Date date = new Date(value.getDateValue());
+ final String transformed = mDateFormat.format(date);
+ if (sDebug) Log.d(TAG, "Transformed " + date + " to " + transformed);
+
+ parentTemplate.setCharSequence(childViewId, "setText", transformed);
+ } catch (Exception e) {
+ Log.w(TAG, "Could not apply " + mDateFormat + " to " + value + ": " + e);
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "DateTransformation: [id=" + mFieldId + ", format=" + mDateFormat + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mFieldId, flags);
+ parcel.writeSerializable(mDateFormat);
+ }
+
+ public static final Parcelable.Creator<DateTransformation> CREATOR =
+ new Parcelable.Creator<DateTransformation>() {
+ @Override
+ public DateTransformation createFromParcel(Parcel parcel) {
+ return new DateTransformation(parcel.readParcelable(null),
+ (DateFormat) parcel.readSerializable());
+ }
+
+ @Override
+ public DateTransformation[] newArray(int size) {
+ return new DateTransformation[size];
+ }
+ };
+}
diff --git a/core/java/android/service/autofill/DateValueSanitizer.java b/core/java/android/service/autofill/DateValueSanitizer.java
new file mode 100644
index 0000000..0f7b540
--- /dev/null
+++ b/core/java/android/service/autofill/DateValueSanitizer.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.Preconditions;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * Sanitizes a date {@link AutofillValue} using a {@link DateFormat}.
+ *
+ * <p>For example, to sanitize a credit card expiration date to just its month and year:
+ *
+ * <pre class="prettyprint">
+ * new DateValueSanitizer(new java.text.SimpleDateFormat("MM/yyyy");
+ * </pre>
+ */
+public final class DateValueSanitizer extends InternalSanitizer implements Sanitizer, Parcelable {
+
+ private static final String TAG = "DateValueSanitizer";
+
+ private final DateFormat mDateFormat;
+
+ /**
+ * Default constructor.
+ *
+ * @param dateFormat date format applied to the actual date value of an input field.
+ */
+ public DateValueSanitizer(@NonNull DateFormat dateFormat) {
+ mDateFormat = Preconditions.checkNotNull(dateFormat);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ @Nullable
+ public AutofillValue sanitize(@NonNull AutofillValue value) {
+ if (value == null) {
+ Log.w(TAG, "sanitize() called with null value");
+ return null;
+ }
+ if (!value.isDate()) {
+ if (sDebug) Log.d(TAG, value + " is not a date");
+ return null;
+ }
+
+ try {
+ final Date date = new Date(value.getDateValue());
+
+ // First convert it to string
+ final String converted = mDateFormat.format(date);
+ if (sDebug) Log.d(TAG, "Transformed " + date + " to " + converted);
+ // Then parse it back to date
+ final Date sanitized = mDateFormat.parse(converted);
+ if (sDebug) Log.d(TAG, "Sanitized to " + sanitized);
+ return AutofillValue.forDate(sanitized.getTime());
+ } catch (Exception e) {
+ Log.w(TAG, "Could not apply " + mDateFormat + " to " + value + ": " + e);
+ return null;
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "DateValueSanitizer: [dateFormat=" + mDateFormat + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeSerializable(mDateFormat);
+ }
+
+ public static final Parcelable.Creator<DateValueSanitizer> CREATOR =
+ new Parcelable.Creator<DateValueSanitizer>() {
+ @Override
+ public DateValueSanitizer createFromParcel(Parcel parcel) {
+ return new DateValueSanitizer((DateFormat) parcel.readSerializable());
+ }
+
+ @Override
+ public DateValueSanitizer[] newArray(int size) {
+ return new DateValueSanitizer[size];
+ }
+ };
+}
diff --git a/core/java/android/service/autofill/ValueFinder.java b/core/java/android/service/autofill/ValueFinder.java
index 1705b7d..7f195d6 100644
--- a/core/java/android/service/autofill/ValueFinder.java
+++ b/core/java/android/service/autofill/ValueFinder.java
@@ -19,6 +19,7 @@
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
/**
* Helper object used to obtain the value of a field in the screen being autofilled.
@@ -29,7 +30,17 @@
public interface ValueFinder {
/**
+ * Gets the value of a field as String, or {@code null} when not found.
+ */
+ @Nullable
+ default String findByAutofillId(@NonNull AutofillId id) {
+ final AutofillValue value = findRawValueByAutofillId(id);
+ return (value == null || !value.isText()) ? null : value.getTextValue().toString();
+ }
+
+ /**
* Gets the value of a field, or {@code null} when not found.
*/
- @Nullable String findByAutofillId(@NonNull AutofillId id);
+ @Nullable
+ AutofillValue findRawValueByAutofillId(@NonNull AutofillId id);
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 6b44fa5..6108afa 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -111,7 +111,7 @@
* until the user authenticates or it times out.
*/
final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener,
- AutoFillUI.AutoFillUiCallback {
+ AutoFillUI.AutoFillUiCallback, ValueFinder {
private static final String TAG = "AutofillSession";
private static final String EXTRA_REQUEST_ID = "android.service.autofill.extra.REQUEST_ID";
@@ -310,43 +310,56 @@
return ids;
}
- /**
- * Gets the value of a field, using either the {@code viewStates} or the {@code mContexts}, or
- * {@code null} when not found on either of them.
- */
+ @Override
@Nullable
- private String getValueAsString(@NonNull AutofillId id) {
- AutofillValue value = null;
+ public String findByAutofillId(@NonNull AutofillId id) {
synchronized (mLock) {
- final ViewState state = mViewStates.get(id);
- if (state == null) {
- if (sDebug) Slog.d(TAG, "getValue(): no view state for " + id);
- return null;
- }
- value = state.getCurrentValue();
- if (value == null) {
- if (sDebug) Slog.d(TAG, "getValue(): no current value for " + id);
- value = getValueFromContextsLocked(id);
- }
- }
- if (value != null) {
- if (value.isText()) {
- return value.getTextValue().toString();
- }
- if (value.isList()) {
- final CharSequence[] options = getAutofillOptionsFromContextsLocked(id);
- if (options != null) {
- final int index = value.getListValue();
- final CharSequence option = options[index];
- return option != null ? option.toString() : null;
- } else {
- Slog.w(TAG, "getValueAsString(): no autofill options for id " + id);
+ AutofillValue value = findValueLocked(id);
+ if (value != null) {
+ if (value.isText()) {
+ return value.getTextValue().toString();
+ }
+
+ if (value.isList()) {
+ final CharSequence[] options = getAutofillOptionsFromContextsLocked(id);
+ if (options != null) {
+ final int index = value.getListValue();
+ final CharSequence option = options[index];
+ return option != null ? option.toString() : null;
+ } else {
+ Slog.w(TAG, "findByAutofillId(): no autofill options for id " + id);
+ }
}
}
}
return null;
}
+ @Override
+ public AutofillValue findRawValueByAutofillId(AutofillId id) {
+ synchronized (mLock) {
+ return findValueLocked(id);
+ }
+ }
+
+ /**
+ * <p>Gets the value of a field, using either the {@code viewStates} or the {@code mContexts},
+ * or {@code null} when not found on either of them.
+ */
+ private AutofillValue findValueLocked(@NonNull AutofillId id) {
+ final ViewState state = mViewStates.get(id);
+ if (state == null) {
+ if (sDebug) Slog.d(TAG, "findValueLocked(): no view state for " + id);
+ return null;
+ }
+ AutofillValue value = state.getCurrentValue();
+ if (value == null) {
+ if (sDebug) Slog.d(TAG, "findValueLocked(): no current value for " + id);
+ value = getValueFromContextsLocked(id);
+ }
+ return value;
+ }
+
/**
* Updates values of the nodes in the context's structure so that:
* - proper node is focused
@@ -1355,14 +1368,12 @@
if (sDebug) {
Slog.d(TAG, "at least one field changed, validate fields for save UI");
}
- final ValueFinder valueFinder = (id) -> {return getValueAsString(id);};
-
final InternalValidator validator = saveInfo.getValidator();
if (validator != null) {
final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_SAVE_VALIDATION);
boolean isValid;
try {
- isValid = validator.isValid(valueFinder);
+ isValid = validator.isValid(this);
if (sDebug) Slog.d(TAG, validator + " returned " + isValid);
log.setType(isValid
? MetricsEvent.TYPE_SUCCESS
@@ -1404,10 +1415,13 @@
}
final AutofillValue datasetValue = datasetValues.get(id);
if (!currentValue.equals(datasetValue)) {
- if (sDebug) Slog.d(TAG, "found a change on id " + id);
+ if (sDebug) {
+ Slog.d(TAG, "found a dataset change on id " + id + ": from "
+ + datasetValue + " to " + currentValue);
+ }
continue datasets_loop;
}
- if (sVerbose) Slog.v(TAG, "no changes for id " + id);
+ if (sVerbose) Slog.v(TAG, "no dataset changes for id " + id);
}
if (sDebug) {
Slog.d(TAG, "ignoring Save UI because all fields match contents of "
@@ -1425,7 +1439,7 @@
final IAutoFillManagerClient client = getClient();
mPendingSaveUi = new PendingUi(mActivityToken, id, client);
getUiForShowing().showSaveUi(mService.getServiceLabel(), mService.getServiceIcon(),
- mService.getServicePackageName(), saveInfo, valueFinder,
+ mService.getServicePackageName(), saveInfo, this,
mComponentName.getPackageName(), this,
mPendingSaveUi);
if (client != null) {