Automatic sources dropoff on 2020-06-10 18:32:38.095721
The change is generated with prebuilt drop tool.
Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/android/service/autofill/AutofillFieldClassificationService.java b/android/service/autofill/AutofillFieldClassificationService.java
new file mode 100644
index 0000000..28842a7
--- /dev/null
+++ b/android/service/autofill/AutofillFieldClassificationService.java
@@ -0,0 +1,372 @@
+/*
+ * 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 com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A service that calculates field classification scores.
+ *
+ * <p>A field classification score is a {@code float} representing how well an
+ * {@link AutofillValue} filled matches a expected value predicted by an autofill service
+ * —a full match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
+ *
+ * <p>The exact score depends on the algorithm used to calculate it—the service must provide
+ * at least one default algorithm (which is used when the algorithm is not specified or is invalid),
+ * but it could provide more (in which case the algorithm name should be specified by the caller
+ * when calculating the scores).
+ *
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public abstract class AutofillFieldClassificationService extends Service {
+
+ private static final String TAG = "AutofillFieldClassificationService";
+
+ /**
+ * The {@link Intent} action that must be declared as handled by a service
+ * in its manifest for the system to recognize it as a quota providing service.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.autofill.AutofillFieldClassificationService";
+
+ /**
+ * Manifest metadata key for the resource string containing the name of the default field
+ * classification algorithm.
+ */
+ public static final String SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM =
+ "android.autofill.field_classification.default_algorithm";
+ /**
+ * Manifest metadata key for the resource string array containing the names of all field
+ * classification algorithms provided by the service.
+ */
+ public static final String SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS =
+ "android.autofill.field_classification.available_algorithms";
+
+ /**
+ * Field classification algorithm that computes the edit distance between two Strings.
+ *
+ * <p>Service implementation must provide this algorithm.</p>
+ */
+ public static final String REQUIRED_ALGORITHM_EDIT_DISTANCE = "EDIT_DISTANCE";
+
+ /**
+ * Field classification algorithm that computes whether the last four digits between two
+ * Strings match exactly.
+ *
+ * <p>Service implementation must provide this algorithm.</p>
+ */
+ public static final String REQUIRED_ALGORITHM_EXACT_MATCH = "EXACT_MATCH";
+
+ /**
+ * Field classification algorithm that compares a credit card string to known last four digits.
+ *
+ * <p>Service implementation must provide this algorithm.</p>
+ */
+ public static final String REQUIRED_ALGORITHM_CREDIT_CARD = "CREDIT_CARD";
+
+ /** {@hide} **/
+ public static final String EXTRA_SCORES = "scores";
+
+ private AutofillFieldClassificationServiceWrapper mWrapper;
+
+ private void calculateScores(RemoteCallback callback, List<AutofillValue> actualValues,
+ String[] userDataValues, String[] categoryIds, String defaultAlgorithm,
+ Bundle defaultArgs, Map algorithms, Map args) {
+ final Bundle data = new Bundle();
+ final float[][] scores = onCalculateScores(actualValues, Arrays.asList(userDataValues),
+ Arrays.asList(categoryIds), defaultAlgorithm, defaultArgs, algorithms, args);
+ if (scores != null) {
+ data.putParcelable(EXTRA_SCORES, new Scores(scores));
+ }
+ callback.sendResult(data);
+ }
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
+
+ /** @hide */
+ @SystemApi
+ @TestApi
+ public AutofillFieldClassificationService() {
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mWrapper = new AutofillFieldClassificationServiceWrapper();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mWrapper;
+ }
+
+ /**
+ * Calculates field classification scores in a batch.
+ *
+ * <p>A field classification score is a {@code float} representing how well an
+ * {@link AutofillValue} filled matches a expected value predicted by an autofill service
+ * —a full match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
+ *
+ * <p>The exact score depends on the algorithm used to calculate it—the service must
+ * provide at least one default algorithm (which is used when the algorithm is not specified
+ * or is invalid), but it could provide more (in which case the algorithm name should be
+ * specified by the caller when calculating the scores).
+ *
+ * <p>For example, if the service provides an algorithm named {@code EXACT_MATCH} that
+ * returns {@code 1.0} if all characters match or {@code 0.0} otherwise, a call to:
+ *
+ * <pre>
+ * service.onGetScores("EXACT_MATCH", null,
+ * Arrays.asList(AutofillValue.forText("email1"), AutofillValue.forText("PHONE1")),
+ * Arrays.asList("email1", "phone1"));
+ * </pre>
+ *
+ * <p>Returns:
+ *
+ * <pre>
+ * [
+ * [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
+ * [0.0, 0.0] // "PHONE1" compared against ["email1", "phone1"]
+ * ];
+ * </pre>
+ *
+ * <p>If the same algorithm allows the caller to specify whether the comparisons should be
+ * case sensitive by passing a boolean option named {@code "case_sensitive"}, then a call to:
+ *
+ * <pre>
+ * Bundle algorithmOptions = new Bundle();
+ * algorithmOptions.putBoolean("case_sensitive", false);
+ *
+ * service.onGetScores("EXACT_MATCH", algorithmOptions,
+ * Arrays.asList(AutofillValue.forText("email1"), AutofillValue.forText("PHONE1")),
+ * Arrays.asList("email1", "phone1"));
+ * </pre>
+ *
+ * <p>Returns:
+ *
+ * <pre>
+ * [
+ * [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
+ * [0.0, 1.0] // "PHONE1" compared against ["email1", "phone1"]
+ * ];
+ * </pre>
+ *
+ * @param algorithm name of the algorithm to be used to calculate the scores. If invalid or
+ * {@code null}, the default algorithm is used instead.
+ * @param algorithmOptions optional arguments to be passed to the algorithm.
+ * @param actualValues values entered by the user.
+ * @param userDataValues values predicted from the user data.
+ * @return the calculated scores of {@code actualValues} x {@code userDataValues}.
+ *
+ * {@hide}
+ *
+ * @deprecated Use {@link AutofillFieldClassificationService#onCalculateScores} instead.
+ */
+ @Nullable
+ @SystemApi
+ @Deprecated
+ public float[][] onGetScores(@Nullable String algorithm,
+ @Nullable Bundle algorithmOptions, @NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues) {
+ Log.e(TAG, "service implementation (" + getClass() + " does not implement onGetScores()");
+ return null;
+ }
+
+ /**
+ * Calculates field classification scores in a batch.
+ *
+ * <p>A field classification score is a {@code float} representing how well an
+ * {@link AutofillValue} matches a expected value predicted by an autofill service
+ * —a full match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
+ *
+ * <p>The exact score depends on the algorithm used to calculate it—the service must
+ * provide at least one default algorithm (which is used when the algorithm is not specified
+ * or is invalid), but it could provide more (in which case the algorithm name should be
+ * specified by the caller when calculating the scores).
+ *
+ * <p>For example, if the service provides an algorithm named {@code EXACT_MATCH} that
+ * returns {@code 1.0} if all characters match or {@code 0.0} otherwise, a call to:
+ *
+ * <pre>
+ * HashMap algorithms = new HashMap<>();
+ * algorithms.put("email", "EXACT_MATCH");
+ * algorithms.put("phone", "EXACT_MATCH");
+ *
+ * HashMap args = new HashMap<>();
+ * args.put("email", null);
+ * args.put("phone", null);
+ *
+ * service.onCalculateScores(Arrays.asList(AutofillValue.forText("email1"),
+ * AutofillValue.forText("PHONE1")), Arrays.asList("email1", "phone1"),
+ * Array.asList("email", "phone"), algorithms, args);
+ * </pre>
+ *
+ * <p>Returns:
+ *
+ * <pre>
+ * [
+ * [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
+ * [0.0, 0.0] // "PHONE1" compared against ["email1", "phone1"]
+ * ];
+ * </pre>
+ *
+ * <p>If the same algorithm allows the caller to specify whether the comparisons should be
+ * case sensitive by passing a boolean option named {@code "case_sensitive"}, then a call to:
+ *
+ * <pre>
+ * Bundle algorithmOptions = new Bundle();
+ * algorithmOptions.putBoolean("case_sensitive", false);
+ * args.put("phone", algorithmOptions);
+ *
+ * service.onCalculateScores(Arrays.asList(AutofillValue.forText("email1"),
+ * AutofillValue.forText("PHONE1")), Arrays.asList("email1", "phone1"),
+ * Array.asList("email", "phone"), algorithms, args);
+ * </pre>
+ *
+ * <p>Returns:
+ *
+ * <pre>
+ * [
+ * [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
+ * [0.0, 1.0] // "PHONE1" compared against ["email1", "phone1"]
+ * ];
+ * </pre>
+ *
+ * @param actualValues values entered by the user.
+ * @param userDataValues values predicted from the user data.
+ * @param categoryIds category Ids correspoinding to userDataValues
+ * @param defaultAlgorithm default field classification algorithm
+ * @param algorithms array of field classification algorithms
+ * @return the calculated scores of {@code actualValues} x {@code userDataValues}.
+ *
+ * {@hide}
+ */
+ @Nullable
+ @SystemApi
+ public float[][] onCalculateScores(@NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues, @NonNull List<String> categoryIds,
+ @Nullable String defaultAlgorithm, @Nullable Bundle defaultArgs,
+ @Nullable Map algorithms, @Nullable Map args) {
+ Log.e(TAG, "service implementation (" + getClass()
+ + " does not implement onCalculateScore()");
+ return null;
+ }
+
+ private final class AutofillFieldClassificationServiceWrapper
+ extends IAutofillFieldClassificationService.Stub {
+ @Override
+ public void calculateScores(RemoteCallback callback, List<AutofillValue> actualValues,
+ String[] userDataValues, String[] categoryIds, String defaultAlgorithm,
+ Bundle defaultArgs, Map algorithms, Map args)
+ throws RemoteException {
+ mHandler.sendMessage(obtainMessage(
+ AutofillFieldClassificationService::calculateScores,
+ AutofillFieldClassificationService.this,
+ callback, actualValues, userDataValues, categoryIds, defaultAlgorithm,
+ defaultArgs, algorithms, args));
+ }
+ }
+
+ /**
+ * Helper class used to encapsulate a float[][] in a Parcelable.
+ *
+ * {@hide}
+ */
+ public static final class Scores implements Parcelable {
+ @NonNull
+ public final float[][] scores;
+
+ private Scores(Parcel parcel) {
+ final int size1 = parcel.readInt();
+ final int size2 = parcel.readInt();
+ scores = new float[size1][size2];
+ for (int i = 0; i < size1; i++) {
+ for (int j = 0; j < size2; j++) {
+ scores[i][j] = parcel.readFloat();
+ }
+ }
+ }
+
+ private Scores(@NonNull float[][] scores) {
+ this.scores = scores;
+ }
+
+ @Override
+ public String toString() {
+ final int size1 = scores.length;
+ final int size2 = size1 > 0 ? scores[0].length : 0;
+ final StringBuilder builder = new StringBuilder("Scores [")
+ .append(size1).append("x").append(size2).append("] ");
+ for (int i = 0; i < size1; i++) {
+ builder.append(i).append(": ").append(Arrays.toString(scores[i])).append(' ');
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ int size1 = scores.length;
+ int size2 = scores[0].length;
+ parcel.writeInt(size1);
+ parcel.writeInt(size2);
+ for (int i = 0; i < size1; i++) {
+ for (int j = 0; j < size2; j++) {
+ parcel.writeFloat(scores[i][j]);
+ }
+ }
+ }
+
+ public static final @android.annotation.NonNull Creator<Scores> CREATOR = new Creator<Scores>() {
+ @Override
+ public Scores createFromParcel(Parcel parcel) {
+ return new Scores(parcel);
+ }
+
+ @Override
+ public Scores[] newArray(int size) {
+ return new Scores[size];
+ }
+ };
+ }
+}
diff --git a/android/service/autofill/AutofillService.java b/android/service/autofill/AutofillService.java
new file mode 100644
index 0000000..188670d
--- /dev/null
+++ b/android/service/autofill/AutofillService.java
@@ -0,0 +1,714 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.app.Service;
+import android.content.Intent;
+import android.os.BaseBundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+/**
+ * An {@code AutofillService} is a service used to automatically fill the contents of the screen
+ * on behalf of a given user - for more information about autofill, read
+ * <a href="{@docRoot}preview/features/autofill.html">Autofill Framework</a>.
+ *
+ * <p>An {@code AutofillService} is only bound to the Android System for autofill purposes if:
+ * <ol>
+ * <li>It requires the {@code android.permission.BIND_AUTOFILL_SERVICE} permission in its
+ * manifest.
+ * <li>The user explicitly enables it using Android Settings (the
+ * {@link Settings#ACTION_REQUEST_SET_AUTOFILL_SERVICE} intent can be used to launch such
+ * Settings screen).
+ * </ol>
+ *
+ * <a name="BasicUsage"></a>
+ * <h3>Basic usage</h3>
+ *
+ * <p>The basic autofill process is defined by the workflow below:
+ * <ol>
+ * <li>User focus an editable {@link View}.
+ * <li>View calls {@link AutofillManager#notifyViewEntered(android.view.View)}.
+ * <li>A {@link ViewStructure} representing all views in the screen is created.
+ * <li>The Android System binds to the service and calls {@link #onConnected()}.
+ * <li>The service receives the view structure through the
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)}.
+ * <li>The service replies through {@link FillCallback#onSuccess(FillResponse)}.
+ * <li>The Android System calls {@link #onDisconnected()} and unbinds from the
+ * {@code AutofillService}.
+ * <li>The Android System displays an autofill UI with the options sent by the service.
+ * <li>The user picks an option.
+ * <li>The proper views are autofilled.
+ * </ol>
+ *
+ * <p>This workflow was designed to minimize the time the Android System is bound to the service;
+ * for each call, it: binds to service, waits for the reply, and unbinds right away. Furthermore,
+ * those calls are considered stateless: if the service needs to keep state between calls, it must
+ * do its own state management (keeping in mind that the service's process might be killed by the
+ * Android System when unbound; for example, if the device is running low in memory).
+ *
+ * <p>Typically, the
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} will:
+ * <ol>
+ * <li>Parse the view structure looking for autofillable views (for example, using
+ * {@link android.app.assist.AssistStructure.ViewNode#getAutofillHints()}.
+ * <li>Match the autofillable views with the user's data.
+ * <li>Create a {@link Dataset} for each set of user's data that match those fields.
+ * <li>Fill the dataset(s) with the proper {@link AutofillId}s and {@link AutofillValue}s.
+ * <li>Add the dataset(s) to the {@link FillResponse} passed to
+ * {@link FillCallback#onSuccess(FillResponse)}.
+ * </ol>
+ *
+ * <p>For example, for a login screen with username and password views where the user only has one
+ * account in the service, the response could be:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder()
+ * .setValue(id1, AutofillValue.forText("homer"), createPresentation("homer"))
+ * .setValue(id2, AutofillValue.forText("D'OH!"), createPresentation("password for homer"))
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>But if the user had 2 accounts instead, the response could be:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder()
+ * .setValue(id1, AutofillValue.forText("homer"), createPresentation("homer"))
+ * .setValue(id2, AutofillValue.forText("D'OH!"), createPresentation("password for homer"))
+ * .build())
+ * .addDataset(new Dataset.Builder()
+ * .setValue(id1, AutofillValue.forText("flanders"), createPresentation("flanders"))
+ * .setValue(id2, AutofillValue.forText("OkelyDokelyDo"), createPresentation("password for flanders"))
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>If the service does not find any autofillable view in the view structure, it should pass
+ * {@code null} to {@link FillCallback#onSuccess(FillResponse)}; if the service encountered an error
+ * processing the request, it should call {@link FillCallback#onFailure(CharSequence)}. For
+ * performance reasons, it's paramount that the service calls either
+ * {@link FillCallback#onSuccess(FillResponse)} or {@link FillCallback#onFailure(CharSequence)} for
+ * each {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} received - if it
+ * doesn't, the request will eventually time out and be discarded by the Android System.
+ *
+ * <a name="SavingUserData"></a>
+ * <h3>Saving user data</h3>
+ *
+ * <p>If the service is also interested on saving the data filled by the user, it must set a
+ * {@link SaveInfo} object in the {@link FillResponse}. See {@link SaveInfo} for more details and
+ * examples.
+ *
+ * <a name="UserAuthentication"></a>
+ * <h3>User authentication</h3>
+ *
+ * <p>The service can provide an extra degree of security by requiring the user to authenticate
+ * before an app can be autofilled. The authentication is typically required in 2 scenarios:
+ * <ul>
+ * <li>To unlock the user data (for example, using a master password or fingerprint
+ * authentication) - see
+ * {@link FillResponse.Builder#setAuthentication(AutofillId[], android.content.IntentSender, android.widget.RemoteViews)}.
+ * <li>To unlock a specific dataset (for example, by providing a CVC for a credit card) - see
+ * {@link Dataset.Builder#setAuthentication(android.content.IntentSender)}.
+ * </ul>
+ *
+ * <p>When using authentication, it is recommended to encrypt only the sensitive data and leave
+ * labels unencrypted, so they can be used on presentation views. For example, if the user has a
+ * home and a work address, the {@code Home} and {@code Work} labels should be stored unencrypted
+ * (since they don't have any sensitive data) while the address data per se could be stored in an
+ * encrypted storage. Then when the user chooses the {@code Home} dataset, the platform starts
+ * the authentication flow, and the service can decrypt the sensitive data.
+ *
+ * <p>The authentication mechanism can also be used in scenarios where the service needs multiple
+ * steps to determine the datasets that can fill a screen. For example, when autofilling a financial
+ * app where the user has accounts for multiple banks, the workflow could be:
+ *
+ * <ol>
+ * <li>The first {@link FillResponse} contains datasets with the credentials for the financial
+ * app, plus a "fake" dataset whose presentation says "Tap here for banking apps credentials".
+ * <li>When the user selects the fake dataset, the service displays a dialog with available
+ * banking apps.
+ * <li>When the user select a banking app, the service replies with a new {@link FillResponse}
+ * containing the datasets for that bank.
+ * </ol>
+ *
+ * <p>Another example of multiple-steps dataset selection is when the service stores the user
+ * credentials in "vaults": the first response would contain fake datasets with the vault names,
+ * and the subsequent response would contain the app credentials stored in that vault.
+ *
+ * <a name="DataPartioning"></a>
+ * <h3>Data partitioning</h3>
+ *
+ * <p>The autofillable views in a screen should be grouped in logical groups called "partitions".
+ * Typical partitions are:
+ * <ul>
+ * <li>Credentials (username/email address, password).
+ * <li>Address (street, city, state, zip code, etc).
+ * <li>Payment info (credit card number, expiration date, and verification code).
+ * </ul>
+ * <p>For security reasons, when a screen has more than one partition, it's paramount that the
+ * contents of a dataset do not spawn multiple partitions, specially when one of the partitions
+ * contains data that is not specific to the application being autofilled. For example, a dataset
+ * should not contain fields for username, password, and credit card information. The reason for
+ * this rule is that a malicious app could draft a view structure where the credit card fields
+ * are not visible, so when the user selects a dataset from the username UI, the credit card info is
+ * released to the application without the user knowledge. Similarly, it's recommended to always
+ * protect a dataset that contains sensitive information by requiring dataset authentication
+ * (see {@link Dataset.Builder#setAuthentication(android.content.IntentSender)}), and to include
+ * info about the "primary" field of the partition in the custom presentation for "secondary"
+ * fields—that would prevent a malicious app from getting the "primary" fields without the
+ * user realizing they're being released (for example, a malicious app could have fields for a
+ * credit card number, verification code, and expiration date crafted in a way that just the latter
+ * is visible; by explicitly indicating the expiration date is related to a given credit card
+ * number, the service would be providing a visual clue for the users to check what would be
+ * released upon selecting that field).
+ *
+ * <p>When the service detects that a screen has multiple partitions, it should return a
+ * {@link FillResponse} with just the datasets for the partition that originated the request (i.e.,
+ * the partition that has the {@link android.app.assist.AssistStructure.ViewNode} whose
+ * {@link android.app.assist.AssistStructure.ViewNode#isFocused()} returns {@code true}); then if
+ * the user selects a field from a different partition, the Android System will make another
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} call for that partition,
+ * and so on.
+ *
+ * <p>Notice that when the user autofill a partition with the data provided by the service and the
+ * user did not change these fields, the autofilled value is sent back to the service in the
+ * subsequent calls (and can be obtained by calling
+ * {@link android.app.assist.AssistStructure.ViewNode#getAutofillValue()}). This is useful in the
+ * cases where the service must create datasets for a partition based on the choice made in a
+ * previous partition. For example, the 1st response for a screen that have credentials and address
+ * partitions could be:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder() // partition 1 (credentials)
+ * .setValue(id1, AutofillValue.forText("homer"), createPresentation("homer"))
+ * .setValue(id2, AutofillValue.forText("D'OH!"), createPresentation("password for homer"))
+ * .build())
+ * .addDataset(new Dataset.Builder() // partition 1 (credentials)
+ * .setValue(id1, AutofillValue.forText("flanders"), createPresentation("flanders"))
+ * .setValue(id2, AutofillValue.forText("OkelyDokelyDo"), createPresentation("password for flanders"))
+ * .build())
+ * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD,
+ * new AutofillId[] { id1, id2 })
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>Then if the user selected {@code flanders}, the service would get a new
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} call, with the values of
+ * the fields {@code id1} and {@code id2} prepopulated, so the service could then fetch the address
+ * for the Flanders account and return the following {@link FillResponse} for the address partition:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder() // partition 2 (address)
+ * .setValue(id3, AutofillValue.forText("744 Evergreen Terrace"), createPresentation("744 Evergreen Terrace")) // street
+ * .setValue(id4, AutofillValue.forText("Springfield"), createPresentation("Springfield")) // city
+ * .build())
+ * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD | SaveInfo.SAVE_DATA_TYPE_ADDRESS,
+ * new AutofillId[] { id1, id2 }) // username and password
+ * .setOptionalIds(new AutofillId[] { id3, id4 }) // state and zipcode
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>When the service returns multiple {@link FillResponse}, the last one overrides the previous;
+ * that's why the {@link SaveInfo} in the 2nd request above has the info for both partitions.
+ *
+ * <a name="PackageVerification"></a>
+ * <h3>Package verification</h3>
+ *
+ * <p>When autofilling app-specific data (like username and password), the service must verify
+ * the authenticity of the request by obtaining all signing certificates of the app being
+ * autofilled, and only fulfilling the request when they match the values that were
+ * obtained when the data was first saved — such verification is necessary to avoid phishing
+ * attempts by apps that were sideloaded in the device with the same package name of another app.
+ * Here's an example on how to achieve that by hashing the signing certificates:
+ *
+ * <pre class="prettyprint">
+ * private String getCertificatesHash(String packageName) throws Exception {
+ * PackageManager pm = mContext.getPackageManager();
+ * PackageInfo info = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
+ * ArrayList<String> hashes = new ArrayList<>(info.signatures.length);
+ * for (Signature sig : info.signatures) {
+ * byte[] cert = sig.toByteArray();
+ * MessageDigest md = MessageDigest.getInstance("SHA-256");
+ * md.update(cert);
+ * hashes.add(toHexString(md.digest()));
+ * }
+ * Collections.sort(hashes);
+ * StringBuilder hash = new StringBuilder();
+ * for (int i = 0; i < hashes.size(); i++) {
+ * hash.append(hashes.get(i));
+ * }
+ * return hash.toString();
+ * }
+ * </pre>
+ *
+ * <p>If the service did not store the signing certificates data the first time the data was saved
+ * — for example, because the data was created by a previous version of the app that did not
+ * use the Autofill Framework — the service should warn the user that the authenticity of the
+ * app cannot be confirmed (see an example on how to show such warning in the
+ * <a href="#WebSecurityDisclaimer">Web security</a> section below), and if the user agrees,
+ * then the service could save the data from the signing ceriticates for future use.
+ *
+ * <a name="IgnoringViews"></a>
+ * <h3>Ignoring views</h3>
+ *
+ * <p>If the service find views that cannot be autofilled (for example, a text field representing
+ * the response to a Captcha challenge), it should mark those views as ignored by
+ * calling {@link FillResponse.Builder#setIgnoredIds(AutofillId...)} so the system does not trigger
+ * a new {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} when these views are
+ * focused.
+ *
+ * <a name="WebSecurity"></a>
+ * <h3>Web security</h3>
+ *
+ * <p>When handling autofill requests that represent web pages (typically
+ * view structures whose root's {@link android.app.assist.AssistStructure.ViewNode#getClassName()}
+ * is a {@link android.webkit.WebView}), the service should take the following steps to verify if
+ * the structure can be autofilled with the data associated with the app requesting it:
+ *
+ * <ol>
+ * <li>Use the {@link android.app.assist.AssistStructure.ViewNode#getWebDomain()} to get the
+ * source of the document.
+ * <li>Get the canonical domain using the
+ * <a href="https://publicsuffix.org/">Public Suffix List</a> (see example below).
+ * <li>Use <a href="https://developers.google.com/digital-asset-links/">Digital Asset Links</a>
+ * to obtain the package name and certificate fingerprint of the package corresponding to
+ * the canonical domain.
+ * <li>Make sure the certificate fingerprint matches the value returned by Package Manager
+ * (see "Package verification" section above).
+ * </ol>
+ *
+ * <p>Here's an example on how to get the canonical domain using
+ * <a href="https://github.com/google/guava">Guava</a>:
+ *
+ * <pre class="prettyprint">
+ * private static String getCanonicalDomain(String domain) {
+ * InternetDomainName idn = InternetDomainName.from(domain);
+ * while (idn != null && !idn.isTopPrivateDomain()) {
+ * idn = idn.parent();
+ * }
+ * return idn == null ? null : idn.toString();
+ * }
+ * </pre>
+ *
+ * <a name="WebSecurityDisclaimer"></a>
+ * <p>If the association between the web domain and app package cannot be verified through the steps
+ * above, but the service thinks that it is appropriate to fill persisted credentials that are
+ * stored for the web domain, the service should warn the user about the potential data
+ * leakage first, and ask for the user to confirm. For example, the service could:
+ *
+ * <ol>
+ * <li>Create a dataset that requires
+ * {@link Dataset.Builder#setAuthentication(android.content.IntentSender) authentication} to
+ * unlock.
+ * <li>Include the web domain in the custom presentation for the
+ * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, android.widget.RemoteViews)
+ * dataset value}.
+ * <li>When the user selects that dataset, show a disclaimer dialog explaining that the app is
+ * requesting credentials for a web domain, but the service could not verify if the app owns
+ * that domain. If the user agrees, then the service can unlock the dataset.
+ * <li>Similarly, when adding a {@link SaveInfo} object for the request, the service should
+ * include the above disclaimer in the {@link SaveInfo.Builder#setDescription(CharSequence)}.
+ * </ol>
+ *
+ * <p>This same procedure could also be used when the autofillable data is contained inside an
+ * {@code IFRAME}, in which case the WebView generates a new autofill context when a node inside
+ * the {@code IFRAME} is focused, with the root node containing the {@code IFRAME}'s {@code src}
+ * attribute on {@link android.app.assist.AssistStructure.ViewNode#getWebDomain()}. A typical and
+ * legitimate use case for this scenario is a financial app that allows the user
+ * to login on different bank accounts. For example, a financial app {@code my_financial_app} could
+ * use a WebView that loads contents from {@code banklogin.my_financial_app.com}, which contains an
+ * {@code IFRAME} node whose {@code src} attribute is {@code login.some_bank.com}. When fulfilling
+ * that request, the service could add an
+ * {@link Dataset.Builder#setAuthentication(android.content.IntentSender) authenticated dataset}
+ * whose presentation displays "Username for some_bank.com" and
+ * "Password for some_bank.com". Then when the user taps one of these options, the service
+ * shows the disclaimer dialog explaining that selecting that option would release the
+ * {@code login.some_bank.com} credentials to the {@code my_financial_app}; if the user agrees,
+ * then the service returns an unlocked dataset with the {@code some_bank.com} credentials.
+ *
+ * <p><b>Note:</b> The autofill service could also whitelist well-known browser apps and skip the
+ * verifications above, as long as the service can verify the authenticity of the browser app by
+ * checking its signing certificate.
+ *
+ * <a name="MultipleStepsSave"></a>
+ * <h3>Saving when data is split in multiple screens</h3>
+ *
+ * Apps often split the user data in multiple screens in the same activity, specially in
+ * activities used to create a new user account. For example, the first screen asks for a username,
+ * and if the username is available, it moves to a second screen, which asks for a password.
+ *
+ * <p>It's tricky to handle save for autofill in these situations, because the autofill service must
+ * wait until the user enters both fields before the autofill save UI can be shown. But it can be
+ * done by following the steps below:
+ *
+ * <ol>
+ * <li>In the first
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback) fill request}, the service
+ * adds a {@link FillResponse.Builder#setClientState(android.os.Bundle) client state bundle} in
+ * the response, containing the autofill ids of the partial fields present in the screen.
+ * <li>In the second
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback) fill request}, the service
+ * retrieves the {@link FillRequest#getClientState() client state bundle}, gets the autofill ids
+ * set in the previous request from the client state, and adds these ids and the
+ * {@link SaveInfo#FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} to the {@link SaveInfo} used in the second
+ * response.
+ * <li>In the {@link #onSaveRequest(SaveRequest, SaveCallback) save request}, the service uses the
+ * proper {@link FillContext fill contexts} to get the value of each field (there is one fill
+ * context per fill request).
+ * </ol>
+ *
+ * <p>For example, in an app that uses 2 steps for the username and password fields, the workflow
+ * would be:
+ * <pre class="prettyprint">
+ * // On first fill request
+ * AutofillId usernameId = // parse from AssistStructure;
+ * Bundle clientState = new Bundle();
+ * clientState.putParcelable("usernameId", usernameId);
+ * fillCallback.onSuccess(
+ * new FillResponse.Builder()
+ * .setClientState(clientState)
+ * .setSaveInfo(new SaveInfo
+ * .Builder(SaveInfo.SAVE_DATA_TYPE_USERNAME, new AutofillId[] {usernameId})
+ * .build())
+ * .build());
+ *
+ * // On second fill request
+ * Bundle clientState = fillRequest.getClientState();
+ * AutofillId usernameId = clientState.getParcelable("usernameId");
+ * AutofillId passwordId = // parse from AssistStructure
+ * clientState.putParcelable("passwordId", passwordId);
+ * fillCallback.onSuccess(
+ * new FillResponse.Builder()
+ * .setClientState(clientState)
+ * .setSaveInfo(new SaveInfo
+ * .Builder(SaveInfo.SAVE_DATA_TYPE_USERNAME | SaveInfo.SAVE_DATA_TYPE_PASSWORD,
+ * new AutofillId[] {usernameId, passwordId})
+ * .setFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE)
+ * .build())
+ * .build());
+ *
+ * // On save request
+ * Bundle clientState = saveRequest.getClientState();
+ * AutofillId usernameId = clientState.getParcelable("usernameId");
+ * AutofillId passwordId = clientState.getParcelable("passwordId");
+ * List<FillContext> fillContexts = saveRequest.getFillContexts();
+ *
+ * FillContext usernameContext = fillContexts.get(0);
+ * ViewNode usernameNode = findNodeByAutofillId(usernameContext.getStructure(), usernameId);
+ * AutofillValue username = usernameNode.getAutofillValue().getTextValue().toString();
+ *
+ * FillContext passwordContext = fillContexts.get(1);
+ * ViewNode passwordNode = findNodeByAutofillId(passwordContext.getStructure(), passwordId);
+ * AutofillValue password = passwordNode.getAutofillValue().getTextValue().toString();
+ *
+ * save(username, password);
+ * </pre>
+ *
+ * <a name="Privacy"></a>
+ * <h3>Privacy</h3>
+ *
+ * <p>The {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} method is called
+ * without the user content. The Android system strips some properties of the
+ * {@link android.app.assist.AssistStructure.ViewNode view nodes} passed to this call, but not all
+ * of them. For example, the data provided in the {@link android.view.ViewStructure.HtmlInfo}
+ * objects set by {@link android.webkit.WebView} is never stripped out.
+ *
+ * <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="FieldClassification"></a>
+ * <h3>Metrics and field classification</h3
+ *
+ * <p>The service can call {@link #getFillEventHistory()} to get metrics representing the user
+ * actions, and then use these metrics to improve its heuristics.
+ *
+ * <p>Prior to Android {@link android.os.Build.VERSION_CODES#P}, the metrics covered just the
+ * scenarios where the service knew how to autofill an activity, but Android
+ * {@link android.os.Build.VERSION_CODES#P} introduced a new mechanism called field classification,
+ * which allows the service to dinamically classify the meaning of fields based on the existing user
+ * data known by the service.
+ *
+ * <p>Typically, field classification can be used to detect fields that can be autofilled with
+ * user data that is not associated with a specific app—such as email and physical
+ * address. Once the service identifies that a such field was manually filled by the user, the
+ * service could use this signal to improve its heuristics on subsequent requests (for example, by
+ * infering which resource ids are associated with known fields).
+ *
+ * <p>The field classification workflow involves 4 steps:
+ *
+ * <ol>
+ * <li>Set the user data through {@link AutofillManager#setUserData(UserData)}. This data is
+ * cached until the system restarts (or the service is disabled), so it doesn't need to be set for
+ * all requests.
+ * <li>Identify which fields should be analysed by calling
+ * {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...)}.
+ * <li>Verify the results through {@link FillEventHistory.Event#getFieldsClassification()}.
+ * <li>Use the results to dynamically create {@link Dataset} or {@link SaveInfo} objects in
+ * subsequent requests.
+ * </ol>
+ *
+ * <p>The field classification is an expensive operation and should be used carefully, otherwise it
+ * can reach its rate limit and get blocked by the Android System. Ideally, it should be used just
+ * in cases where the service could not determine how an activity can be autofilled, but it has a
+ * strong suspicious that it could. For example, if an activity has four or more fields and one of
+ * them is a list, chances are that these are address fields (like address, city, state, and
+ * zip code).
+ *
+ * <a name="CompatibilityMode"></a>
+ * <h3>Compatibility mode</h3>
+ *
+ * <p>Apps that use standard Android widgets support autofill out-of-the-box and need to do
+ * very little to improve their user experience (annotating autofillable views and providing
+ * autofill hints). However, some apps (typically browsers) do their own rendering and the rendered
+ * content may contain semantic structure that needs to be surfaced to the autofill framework. The
+ * platform exposes APIs to achieve this, however it could take some time until these apps implement
+ * autofill support.
+ *
+ * <p>To enable autofill for such apps the platform provides a compatibility mode in which the
+ * platform would fall back to the accessibility APIs to generate the state reported to autofill
+ * services and fill data. This mode needs to be explicitly requested for a given package up
+ * to a specified max version code allowing clean migration path when the target app begins to
+ * support autofill natively. Note that enabling compatibility may degrade performance for the
+ * target package and should be used with caution. The platform supports whitelisting which packages
+ * can be targeted in compatibility mode to ensure this mode is used only when needed and as long
+ * as needed.
+ *
+ * <p>You can request compatibility mode for packages of interest in the meta-data resource
+ * associated with your service. Below is a sample service declaration:
+ *
+ * <pre> <service android:name=".MyAutofillService"
+ * android:permission="android.permission.BIND_AUTOFILL_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.autofill.AutofillService" />
+ * </intent-filter>
+ * <meta-data android:name="android.autofill" android:resource="@xml/autofillservice" />
+ * </service></pre>
+ *
+ * <p>In the XML file you can specify one or more packages for which to enable compatibility
+ * mode. Below is a sample meta-data declaration:
+ *
+ * <pre> <autofill-service xmlns:android="http://schemas.android.com/apk/res/android">
+ * <compatibility-package android:name="foo.bar.baz" android:maxLongVersionCode="1000000000"/>
+ * </autofill-service></pre>
+ *
+ * <p>Notice that compatibility mode has limitations such as:
+ * <ul>
+ * <li>No manual autofill requests. Hence, the {@link FillRequest}
+ * {@link FillRequest#getFlags() flags} never have the {@link FillRequest#FLAG_MANUAL_REQUEST} flag.
+ * <li>The value of password fields are most likely masked—for example, {@code ****} instead
+ * of {@code 1234}. Hence, you must be careful when using these values to avoid updating the user
+ * data with invalid input. For example, when you parse the {@link FillRequest} and detect a
+ * password field, you could check if its
+ * {@link android.app.assist.AssistStructure.ViewNode#getInputType()
+ * input type} has password flags and if so, don't add it to the {@link SaveInfo} object.
+ * <li>The autofill context is not always {@link AutofillManager#commit() committed} when an HTML
+ * form is submitted. Hence, you must use other mechanisms to trigger save, such as setting the
+ * {@link SaveInfo#FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} flag on {@link SaveInfo.Builder#setFlags(int)}
+ * or using {@link SaveInfo.Builder#setTriggerId(AutofillId)}.
+ * <li>Browsers often provide their own autofill management system. When both the browser and
+ * the platform render an autofill dialog at the same time, the result can be confusing to the user.
+ * Such browsers typically offer an option for users to disable autofill, so your service should
+ * also allow users to disable compatiblity mode for specific apps. That way, it is up to the user
+ * to decide which autofill mechanism—the browser's or the platform's—should be used.
+ * </ul>
+ */
+public abstract class AutofillService extends Service {
+ private static final String TAG = "AutofillService";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_AUTOFILL_SERVICE} permission so
+ * that other applications can not abuse it.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE = "android.service.autofill.AutofillService";
+
+ /**
+ * Name under which a AutoFillService component publishes information about itself.
+ * This meta-data should reference an XML resource containing a
+ * <code><{@link
+ * android.R.styleable#AutofillService autofill-service}></code> tag.
+ * This is a a sample XML file configuring an AutoFillService:
+ * <pre> <autofill-service
+ * android:settingsActivity="foo.bar.SettingsActivity"
+ * . . .
+ * /></pre>
+ */
+ public static final String SERVICE_META_DATA = "android.autofill";
+
+ private final IAutoFillService mInterface = new IAutoFillService.Stub() {
+ @Override
+ public void onConnectedStateChanged(boolean connected) {
+ mHandler.sendMessage(obtainMessage(
+ connected ? AutofillService::onConnected : AutofillService::onDisconnected,
+ AutofillService.this));
+ }
+
+ @Override
+ public void onFillRequest(FillRequest request, IFillCallback callback) {
+ ICancellationSignal transport = CancellationSignal.createTransport();
+ try {
+ callback.onCancellable(transport);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ mHandler.sendMessage(obtainMessage(
+ AutofillService::onFillRequest,
+ AutofillService.this, request, CancellationSignal.fromTransport(transport),
+ new FillCallback(callback, request.getId())));
+ }
+
+ @Override
+ public void onSaveRequest(SaveRequest request, ISaveCallback callback) {
+ mHandler.sendMessage(obtainMessage(
+ AutofillService::onSaveRequest,
+ AutofillService.this, request, new SaveCallback(callback)));
+ }
+ };
+
+ private Handler mHandler;
+
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler(Looper.getMainLooper(), null, true);
+ BaseBundle.setShouldDefuse(true);
+ }
+
+ @Override
+ public final IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mInterface.asBinder();
+ }
+ Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+ return null;
+ }
+
+ /**
+ * Called when the Android system connects to service.
+ *
+ * <p>You should generally do initialization here rather than in {@link #onCreate}.
+ */
+ public void onConnected() {
+ }
+
+ /**
+ * Called by the Android system do decide if a screen can be autofilled by the service.
+ *
+ * <p>Service must call one of the {@link FillCallback} methods (like
+ * {@link FillCallback#onSuccess(FillResponse)}
+ * or {@link FillCallback#onFailure(CharSequence)})
+ * to notify the result of the request.
+ *
+ * @param request the {@link FillRequest request} to handle.
+ * See {@link FillResponse} for examples of multiple-sections requests.
+ * @param cancellationSignal signal for observing cancellation requests. The system will use
+ * this to notify you that the fill result is no longer needed and you should stop
+ * handling this fill request in order to save resources.
+ * @param callback object used to notify the result of the request.
+ */
+ public abstract void onFillRequest(@NonNull FillRequest request,
+ @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback);
+
+ /**
+ * Called when the user requests the service to save the contents of a screen.
+ *
+ * <p>If the service could not handle the request right away—for example, because it must
+ * launch an activity asking the user to authenticate first or because the network is
+ * down—the service could keep the {@link SaveRequest request} and reuse it later,
+ * but the service <b>must always</b> call {@link SaveCallback#onSuccess()} or
+ * {@link SaveCallback#onSuccess(android.content.IntentSender)} right away.
+ *
+ * <p><b>Note:</b> To retrieve the actual value of fields input by the user, the service
+ * should call
+ * {@link android.app.assist.AssistStructure.ViewNode#getAutofillValue()}; if it calls
+ * {@link android.app.assist.AssistStructure.ViewNode#getText()} or other methods, there is no
+ * guarantee such method will return the most recent value of the field.
+ *
+ * @param request the {@link SaveRequest request} to handle.
+ * See {@link FillResponse} for examples of multiple-sections requests.
+ * @param callback object used to notify the result of the request.
+ */
+ public abstract void onSaveRequest(@NonNull SaveRequest request,
+ @NonNull SaveCallback callback);
+
+ /**
+ * Called when the Android system disconnects from the service.
+ *
+ * <p> At this point this service may no longer be an active {@link AutofillService}.
+ * It should not make calls on {@link AutofillManager} that requires the caller to be
+ * the current service.
+ */
+ public void onDisconnected() {
+ }
+
+ /**
+ * Gets the events that happened after the last
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}
+ * call.
+ *
+ * <p>This method is typically used to keep track of previous user actions to optimize further
+ * requests. For example, the service might return email addresses in alphabetical order by
+ * default, but change that order based on the address the user picked on previous requests.
+ *
+ * <p>The history is not persisted over reboots, and it's cleared every time the service
+ * replies to a {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} by calling
+ * {@link FillCallback#onSuccess(FillResponse)} or {@link FillCallback#onFailure(CharSequence)}
+ * (if the service doesn't call any of these methods, the history will clear out after some
+ * pre-defined time). Hence, the service should call {@link #getFillEventHistory()} before
+ * finishing the {@link FillCallback}.
+ *
+ * @return The history or {@code null} if there are no events.
+ *
+ * @throws RuntimeException if the event history could not be retrieved.
+ */
+ @Nullable public final FillEventHistory getFillEventHistory() {
+ final AutofillManager afm = getSystemService(AutofillManager.class);
+
+ if (afm == null) {
+ return null;
+ } else {
+ return afm.getFillEventHistory();
+ }
+ }
+}
diff --git a/android/service/autofill/AutofillServiceHelper.java b/android/service/autofill/AutofillServiceHelper.java
new file mode 100644
index 0000000..13fedba
--- /dev/null
+++ b/android/service/autofill/AutofillServiceHelper.java
@@ -0,0 +1,41 @@
+/*
+ * 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 android.annotation.Nullable;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.util.Preconditions;
+
+/** @hide */
+final class AutofillServiceHelper {
+
+ static AutofillId[] assertValid(@Nullable AutofillId[] ids) {
+ Preconditions.checkArgument(ids != null && ids.length > 0, "must have at least one id");
+ // Can't use Preconditions.checkArrayElementsNotNull() because it throws NPE instead of IAE
+ for (int i = 0; i < ids.length; ++i) {
+ if (ids[i] == null) {
+ throw new IllegalArgumentException("ids[" + i + "] must not be null");
+ }
+ }
+ return ids;
+ }
+
+ private AutofillServiceHelper() {
+ throw new UnsupportedOperationException("contains static members only");
+ }
+}
diff --git a/android/service/autofill/AutofillServiceInfo.java b/android/service/autofill/AutofillServiceInfo.java
new file mode 100644
index 0000000..fbc25a6
--- /dev/null
+++ b/android/service/autofill/AutofillServiceInfo.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.metrics.LogMaker;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.internal.R;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * {@link ServiceInfo} and meta-data about an {@link AutofillService}.
+ *
+ * @hide
+ */
+public final class AutofillServiceInfo {
+ private static final String TAG = "AutofillServiceInfo";
+
+ private static final String TAG_AUTOFILL_SERVICE = "autofill-service";
+ private static final String TAG_COMPATIBILITY_PACKAGE = "compatibility-package";
+
+ private static ServiceInfo getServiceInfoOrThrow(ComponentName comp, int userHandle)
+ throws PackageManager.NameNotFoundException {
+ try {
+ ServiceInfo si = AppGlobals.getPackageManager().getServiceInfo(
+ comp,
+ PackageManager.GET_META_DATA,
+ userHandle);
+ if (si != null) {
+ return si;
+ }
+ } catch (RemoteException e) {
+ }
+ throw new PackageManager.NameNotFoundException(comp.toString());
+ }
+
+ @NonNull
+ private final ServiceInfo mServiceInfo;
+
+ @Nullable
+ private final String mSettingsActivity;
+
+ @Nullable
+ private final ArrayMap<String, Long> mCompatibilityPackages;
+
+ private final boolean mInlineSuggestionsEnabled;
+
+ public AutofillServiceInfo(Context context, ComponentName comp, int userHandle)
+ throws PackageManager.NameNotFoundException {
+ this(context, getServiceInfoOrThrow(comp, userHandle));
+ }
+
+ public AutofillServiceInfo(Context context, ServiceInfo si) {
+ // Check for permissions.
+ if (!Manifest.permission.BIND_AUTOFILL_SERVICE.equals(si.permission)) {
+ if (Manifest.permission.BIND_AUTOFILL.equals(si.permission)) {
+ // Let it go for now...
+ Log.w(TAG, "AutofillService from '" + si.packageName + "' uses unsupported "
+ + "permission " + Manifest.permission.BIND_AUTOFILL + ". It works for "
+ + "now, but might not be supported on future releases");
+ new MetricsLogger().write(new LogMaker(MetricsEvent.AUTOFILL_INVALID_PERMISSION)
+ .setPackageName(si.packageName));
+ } else {
+ Log.w(TAG, "AutofillService from '" + si.packageName
+ + "' does not require permission "
+ + Manifest.permission.BIND_AUTOFILL_SERVICE);
+ throw new SecurityException("Service does not require permission "
+ + Manifest.permission.BIND_AUTOFILL_SERVICE);
+ }
+ }
+
+ mServiceInfo = si;
+
+ // Get the AutoFill metadata, if declared.
+ final XmlResourceParser parser = si.loadXmlMetaData(context.getPackageManager(),
+ AutofillService.SERVICE_META_DATA);
+ if (parser == null) {
+ mSettingsActivity = null;
+ mCompatibilityPackages = null;
+ mInlineSuggestionsEnabled = false;
+ return;
+ }
+
+ String settingsActivity = null;
+ ArrayMap<String, Long> compatibilityPackages = null;
+ boolean inlineSuggestionsEnabled = false; // false by default.
+
+ try {
+ final Resources resources = context.getPackageManager().getResourcesForApplication(
+ si.applicationInfo);
+
+ int type = 0;
+ while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
+ type = parser.next();
+ }
+
+ if (TAG_AUTOFILL_SERVICE.equals(parser.getName())) {
+ final AttributeSet allAttributes = Xml.asAttributeSet(parser);
+ TypedArray afsAttributes = null;
+ try {
+ afsAttributes = resources.obtainAttributes(allAttributes,
+ com.android.internal.R.styleable.AutofillService);
+ settingsActivity = afsAttributes.getString(
+ R.styleable.AutofillService_settingsActivity);
+ inlineSuggestionsEnabled = afsAttributes.getBoolean(
+ R.styleable.AutofillService_supportsInlineSuggestions, false);
+ } finally {
+ if (afsAttributes != null) {
+ afsAttributes.recycle();
+ }
+ }
+ compatibilityPackages = parseCompatibilityPackages(parser, resources);
+ } else {
+ Log.e(TAG, "Meta-data does not start with autofill-service tag");
+ }
+ } catch (PackageManager.NameNotFoundException | IOException | XmlPullParserException e) {
+ Log.e(TAG, "Error parsing auto fill service meta-data", e);
+ }
+
+ mSettingsActivity = settingsActivity;
+ mCompatibilityPackages = compatibilityPackages;
+ mInlineSuggestionsEnabled = inlineSuggestionsEnabled;
+ }
+
+ private ArrayMap<String, Long> parseCompatibilityPackages(XmlPullParser parser,
+ Resources resources) throws IOException, XmlPullParserException {
+ ArrayMap<String, Long> compatibilityPackages = null;
+
+ final int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ if (TAG_COMPATIBILITY_PACKAGE.equals(parser.getName())) {
+ TypedArray cpAttributes = null;
+ try {
+ final AttributeSet allAttributes = Xml.asAttributeSet(parser);
+
+ cpAttributes = resources.obtainAttributes(allAttributes,
+ R.styleable.AutofillService_CompatibilityPackage);
+
+ final String name = cpAttributes.getString(
+ R.styleable.AutofillService_CompatibilityPackage_name);
+ if (TextUtils.isEmpty(name)) {
+ Log.e(TAG, "Invalid compatibility package:" + name);
+ break;
+ }
+
+ final String maxVersionCodeStr = cpAttributes.getString(
+ R.styleable.AutofillService_CompatibilityPackage_maxLongVersionCode);
+ final Long maxVersionCode;
+ if (maxVersionCodeStr != null) {
+ try {
+ maxVersionCode = Long.parseLong(maxVersionCodeStr);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Invalid compatibility max version code:"
+ + maxVersionCodeStr);
+ break;
+ }
+ if (maxVersionCode < 0) {
+ Log.e(TAG, "Invalid compatibility max version code:"
+ + maxVersionCode);
+ break;
+ }
+ } else {
+ maxVersionCode = Long.MAX_VALUE;
+ }
+ if (compatibilityPackages == null) {
+ compatibilityPackages = new ArrayMap<>();
+ }
+ compatibilityPackages.put(name, maxVersionCode);
+ } finally {
+ XmlUtils.skipCurrentTag(parser);
+ if (cpAttributes != null) {
+ cpAttributes.recycle();
+ }
+ }
+ }
+ }
+
+ return compatibilityPackages;
+ }
+
+ public ServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ @Nullable
+ public String getSettingsActivity() {
+ return mSettingsActivity;
+ }
+
+ public ArrayMap<String, Long> getCompatibilityPackages() {
+ return mCompatibilityPackages;
+ }
+
+ public boolean isInlineSuggestionsEnabled() {
+ return mInlineSuggestionsEnabled;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(getClass().getSimpleName());
+ builder.append("[").append(mServiceInfo);
+ builder.append(", settings:").append(mSettingsActivity);
+ builder.append(", hasCompatPckgs:").append(mCompatibilityPackages != null
+ && !mCompatibilityPackages.isEmpty()).append("]");
+ builder.append(", inline suggestions enabled:").append(mInlineSuggestionsEnabled);
+ return builder.toString();
+ }
+
+ /**
+ * Dumps it!
+ */
+ public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("Component: "); pw.println(getServiceInfo().getComponentName());
+ pw.print(prefix); pw.print("Settings: "); pw.println(mSettingsActivity);
+ pw.print(prefix); pw.print("Compat packages: "); pw.println(mCompatibilityPackages);
+ pw.print(prefix); pw.print("Inline Suggestions Enabled: ");
+ pw.println(mInlineSuggestionsEnabled);
+ }
+}
diff --git a/android/service/autofill/BatchUpdates.java b/android/service/autofill/BatchUpdates.java
new file mode 100644
index 0000000..e0b1c2f
--- /dev/null
+++ b/android/service/autofill/BatchUpdates.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+
+/**
+ * Defines actions to be applied to a {@link RemoteViews template presentation}.
+ *
+ *
+ * <p>It supports 2 types of actions:
+ *
+ * <ol>
+ * <li>{@link RemoteViews Actions} to be applied to the template.
+ * <li>{@link Transformation Transformations} to be applied on child views.
+ * </ol>
+ *
+ * <p>Typically used on {@link CustomDescription custom descriptions} to conditionally display
+ * differents views based on user input - see
+ * {@link CustomDescription.Builder#batchUpdate(Validator, BatchUpdates)} for more information.
+ */
+public final class BatchUpdates implements Parcelable {
+
+ private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
+ private final RemoteViews mUpdates;
+
+ private BatchUpdates(Builder builder) {
+ mTransformations = builder.mTransformations;
+ mUpdates = builder.mUpdates;
+ }
+
+ /** @hide */
+ @Nullable
+ public ArrayList<Pair<Integer, InternalTransformation>> getTransformations() {
+ return mTransformations;
+ }
+
+ /** @hide */
+ @Nullable
+ public RemoteViews getUpdates() {
+ return mUpdates;
+ }
+
+ /**
+ * Builder for {@link BatchUpdates} objects.
+ */
+ public static class Builder {
+ private RemoteViews mUpdates;
+
+ private boolean mDestroyed;
+ private ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
+
+ /**
+ * Applies the {@code updates} in the underlying presentation template.
+ *
+ * <p><b>Note:</b> The updates are applied before the
+ * {@link #transformChild(int, Transformation) transformations} are applied to the children
+ * views.
+ *
+ * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color
+ * or background color: Autofill on different platforms may have different themes.
+ *
+ * @param updates a {@link RemoteViews} with the updated actions to be applied in the
+ * underlying presentation template.
+ *
+ * @return this builder
+ * @throws IllegalArgumentException if {@code condition} is not a class provided
+ * by the Android System.
+ */
+ public Builder updateTemplate(@NonNull RemoteViews updates) {
+ throwIfDestroyed();
+ mUpdates = Preconditions.checkNotNull(updates);
+ return this;
+ }
+
+ /**
+ * Adds a transformation to replace the value of a child view with the fields in the
+ * screen.
+ *
+ * <p>When multiple transformations are added for the same child view, they are applied
+ * in the same order as added.
+ *
+ * <p><b>Note:</b> The transformations are applied after the
+ * {@link #updateTemplate(RemoteViews) updates} are applied to the presentation template.
+ *
+ * @param id view id of the children view.
+ * @param transformation an implementation provided by the Android System.
+ * @return this builder.
+ * @throws IllegalArgumentException if {@code transformation} is not a class provided
+ * by the Android System.
+ */
+ public Builder transformChild(int id, @NonNull Transformation transformation) {
+ throwIfDestroyed();
+ Preconditions.checkArgument((transformation instanceof InternalTransformation),
+ "not provided by Android System: " + transformation);
+ if (mTransformations == null) {
+ mTransformations = new ArrayList<>();
+ }
+ mTransformations.add(new Pair<>(id, (InternalTransformation) transformation));
+ return this;
+ }
+
+ /**
+ * Creates a new {@link BatchUpdates} instance.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called before or no call
+ * to {@link #updateTemplate(RemoteViews)} or {@link #transformChild(int, Transformation)}
+ * has been made.
+ */
+ public BatchUpdates build() {
+ throwIfDestroyed();
+ Preconditions.checkState(mUpdates != null || mTransformations != null,
+ "must call either updateTemplate() or transformChild() at least once");
+ mDestroyed = true;
+ return new BatchUpdates(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return new StringBuilder("BatchUpdates: [")
+ .append(", transformations=")
+ .append(mTransformations == null ? "N/A" : mTransformations.size())
+ .append(", updates=").append(mUpdates)
+ .append("]").toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mTransformations == null) {
+ dest.writeIntArray(null);
+ } else {
+ final int size = mTransformations.size();
+ final int[] ids = new int[size];
+ final InternalTransformation[] values = new InternalTransformation[size];
+ for (int i = 0; i < size; i++) {
+ final Pair<Integer, InternalTransformation> pair = mTransformations.get(i);
+ ids[i] = pair.first;
+ values[i] = pair.second;
+ }
+ dest.writeIntArray(ids);
+ dest.writeParcelableArray(values, flags);
+ }
+ dest.writeParcelable(mUpdates, flags);
+ }
+ public static final @android.annotation.NonNull Parcelable.Creator<BatchUpdates> CREATOR =
+ new Parcelable.Creator<BatchUpdates>() {
+ @Override
+ public BatchUpdates createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final Builder builder = new Builder();
+ final int[] ids = parcel.createIntArray();
+ if (ids != null) {
+ final InternalTransformation[] values =
+ parcel.readParcelableArray(null, InternalTransformation.class);
+ final int size = ids.length;
+ for (int i = 0; i < size; i++) {
+ builder.transformChild(ids[i], values[i]);
+ }
+ }
+ final RemoteViews updates = parcel.readParcelable(null);
+ if (updates != null) {
+ builder.updateTemplate(updates);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public BatchUpdates[] newArray(int size) {
+ return new BatchUpdates[size];
+ }
+ };
+}
diff --git a/android/service/autofill/CharSequenceTransformation.java b/android/service/autofill/CharSequenceTransformation.java
new file mode 100644
index 0000000..e3e8844
--- /dev/null
+++ b/android/service/autofill/CharSequenceTransformation.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.util.Pair;
+import android.view.autofill.AutofillId;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Replaces a {@link TextView} child of a {@link CustomDescription} with the contents of one or
+ * more regular expressions (regexs).
+ *
+ * <p>When it contains more than one field, the fields that match their regex are added to the
+ * overall transformation result.
+ *
+ * <p>For example, a transformation to mask a credit card number contained in just one field would
+ * be:
+ *
+ * <pre class="prettyprint">
+ * new CharSequenceTransformation
+ * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
+ * .build();
+ * </pre>
+ *
+ * <p>But a transformation that generates a {@code Exp: MM / YYYY} credit expiration date from two
+ * fields (month and year) would be:
+ *
+ * <pre class="prettyprint">
+ * new CharSequenceTransformation
+ * .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
+ * .addField(ccExpYearId, Pattern.compile("^(\\d\\d\\d\\d)$"), " / $1");
+ * </pre>
+ */
+public final class CharSequenceTransformation extends InternalTransformation implements
+ Transformation, Parcelable {
+ private static final String TAG = "CharSequenceTransformation";
+
+ // Must use LinkedHashMap to preserve insertion order.
+ @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields;
+
+ private CharSequenceTransformation(Builder builder) {
+ mFields = builder.mFields;
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
+ int childViewId) throws Exception {
+ final StringBuilder converted = new StringBuilder();
+ final int size = mFields.size();
+ if (sDebug) Log.d(TAG, size + " fields on id " + childViewId);
+ for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
+ final AutofillId id = entry.getKey();
+ final Pair<Pattern, String> field = entry.getValue();
+ final String value = finder.findByAutofillId(id);
+ if (value == null) {
+ Log.w(TAG, "No value for id " + id);
+ return;
+ }
+ try {
+ final Matcher matcher = field.first.matcher(value);
+ if (!matcher.find()) {
+ if (sDebug) Log.d(TAG, "Match for " + field.first + " failed on id " + id);
+ return;
+ }
+ // replaceAll throws an exception if the subst is invalid
+ final String convertedValue = matcher.replaceAll(field.second);
+ converted.append(convertedValue);
+ } catch (Exception e) {
+ // Do not log full exception to avoid PII leaking
+ Log.w(TAG, "Cannot apply " + field.first.pattern() + "->" + field.second + " to "
+ + "field with autofill id" + id + ": " + e.getClass());
+ throw e;
+ }
+ }
+ // Cannot log converted, it might have PII
+ Log.d(TAG, "Converting text on child " + childViewId + " to " + converted.length()
+ + "_chars");
+ parentTemplate.setCharSequence(childViewId, "setText", converted);
+ }
+
+ /**
+ * Builder for {@link CharSequenceTransformation} objects.
+ */
+ public static class Builder {
+
+ // Must use LinkedHashMap to preserve insertion order.
+ @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields =
+ new LinkedHashMap<>();
+ private boolean mDestroyed;
+
+ /**
+ * Creates a new builder and adds the first transformed contents of a field to the overall
+ * result of this transformation.
+ *
+ * @param id id of the screen field.
+ * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
+ * are used to substitute parts of the value.
+ * @param subst the string that substitutes the matched regex, using {@code $} for
+ * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
+ */
+ public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @NonNull String subst) {
+ addField(id, regex, subst);
+ }
+
+ /**
+ * Adds the transformed contents of a field to the overall result of this transformation.
+ *
+ * @param id id of the screen field.
+ * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
+ * are used to substitute parts of the value.
+ * @param subst the string that substitutes the matched regex, using {@code $} for
+ * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
+ *
+ * @return this builder.
+ */
+ public Builder addField(@NonNull AutofillId id, @NonNull Pattern regex,
+ @NonNull String subst) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(id);
+ Preconditions.checkNotNull(regex);
+ Preconditions.checkNotNull(subst);
+
+ mFields.put(id, new Pair<>(regex, subst));
+ return this;
+ }
+
+ /**
+ * Creates a new {@link CharSequenceTransformation} instance.
+ */
+ public CharSequenceTransformation build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new CharSequenceTransformation(this);
+ }
+
+ private void throwIfDestroyed() {
+ Preconditions.checkState(!mDestroyed, "Already called build()");
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "MultipleViewsCharSequenceTransformation: [fields=" + mFields + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ final int size = mFields.size();
+ final AutofillId[] ids = new AutofillId[size];
+ final Pattern[] regexs = new Pattern[size];
+ final String[] substs = new String[size];
+ Pair<Pattern, String> pair;
+ int i = 0;
+ for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
+ ids[i] = entry.getKey();
+ pair = entry.getValue();
+ regexs[i] = pair.first;
+ substs[i] = pair.second;
+ i++;
+ }
+
+ parcel.writeParcelableArray(ids, flags);
+ parcel.writeSerializable(regexs);
+ parcel.writeStringArray(substs);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<CharSequenceTransformation> CREATOR =
+ new Parcelable.Creator<CharSequenceTransformation>() {
+ @Override
+ public CharSequenceTransformation createFromParcel(Parcel parcel) {
+ final AutofillId[] ids = parcel.readParcelableArray(null, AutofillId.class);
+ final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
+ final String[] substs = parcel.createStringArray();
+
+ // 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 CharSequenceTransformation.Builder builder =
+ new CharSequenceTransformation.Builder(ids[0], regexs[0], substs[0]);
+
+ final int size = ids.length;
+ for (int i = 1; i < size; i++) {
+ builder.addField(ids[i], regexs[i], substs[i]);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public CharSequenceTransformation[] newArray(int size) {
+ return new CharSequenceTransformation[size];
+ }
+ };
+}
diff --git a/android/service/autofill/CompositeUserData.java b/android/service/autofill/CompositeUserData.java
new file mode 100644
index 0000000..c7dc15a
--- /dev/null
+++ b/android/service/autofill/CompositeUserData.java
@@ -0,0 +1,211 @@
+/*
+ * 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.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Holds both a generic and package-specific userData used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ *
+ * @hide
+ */
+@TestApi
+public final class CompositeUserData implements FieldClassificationUserData, Parcelable {
+
+ private final UserData mGenericUserData;
+ private final UserData mPackageUserData;
+
+ private final String[] mCategories;
+ private final String[] mValues;
+
+ public CompositeUserData(@Nullable UserData genericUserData,
+ @NonNull UserData packageUserData) {
+ mGenericUserData = genericUserData;
+ mPackageUserData = packageUserData;
+
+ final String[] packageCategoryIds = mPackageUserData.getCategoryIds();
+ final String[] packageValues = mPackageUserData.getValues();
+
+ final ArrayList<String> categoryIds = new ArrayList<>(packageCategoryIds.length);
+ final ArrayList<String> values = new ArrayList<>(packageValues.length);
+
+ Collections.addAll(categoryIds, packageCategoryIds);
+ Collections.addAll(values, packageValues);
+
+ if (mGenericUserData != null) {
+ final String[] genericCategoryIds = mGenericUserData.getCategoryIds();
+ final String[] genericValues = mGenericUserData.getValues();
+ final int size = mGenericUserData.getCategoryIds().length;
+ for (int i = 0; i < size; i++) {
+ if (!categoryIds.contains(genericCategoryIds[i])) {
+ categoryIds.add(genericCategoryIds[i]);
+ values.add(genericValues[i]);
+ }
+ }
+ }
+
+ mCategories = new String[categoryIds.size()];
+ categoryIds.toArray(mCategories);
+ mValues = new String[values.size()];
+ values.toArray(mValues);
+ }
+
+ @Nullable
+ @Override
+ public String getFieldClassificationAlgorithm() {
+ final String packageDefaultAlgo = mPackageUserData.getFieldClassificationAlgorithm();
+ if (packageDefaultAlgo != null) {
+ return packageDefaultAlgo;
+ } else {
+ return mGenericUserData == null ? null :
+ mGenericUserData.getFieldClassificationAlgorithm();
+ }
+ }
+
+ @Override
+ public Bundle getDefaultFieldClassificationArgs() {
+ final Bundle packageDefaultArgs = mPackageUserData.getDefaultFieldClassificationArgs();
+ if (packageDefaultArgs != null) {
+ return packageDefaultArgs;
+ } else {
+ return mGenericUserData == null ? null :
+ mGenericUserData.getDefaultFieldClassificationArgs();
+ }
+ }
+
+ @Nullable
+ @Override
+ public String getFieldClassificationAlgorithmForCategory(@NonNull String categoryId) {
+ Preconditions.checkNotNull(categoryId);
+ final ArrayMap<String, String> categoryAlgorithms = getFieldClassificationAlgorithms();
+ if (categoryAlgorithms == null || !categoryAlgorithms.containsKey(categoryId)) {
+ return null;
+ }
+ return categoryAlgorithms.get(categoryId);
+ }
+
+ @Override
+ public ArrayMap<String, String> getFieldClassificationAlgorithms() {
+ final ArrayMap<String, String> packageAlgos = mPackageUserData
+ .getFieldClassificationAlgorithms();
+ final ArrayMap<String, String> genericAlgos = mGenericUserData == null ? null :
+ mGenericUserData.getFieldClassificationAlgorithms();
+
+ ArrayMap<String, String> categoryAlgorithms = null;
+ if (packageAlgos != null || genericAlgos != null) {
+ categoryAlgorithms = new ArrayMap<>();
+ if (genericAlgos != null) {
+ categoryAlgorithms.putAll(genericAlgos);
+ }
+ if (packageAlgos != null) {
+ categoryAlgorithms.putAll(packageAlgos);
+ }
+ }
+
+ return categoryAlgorithms;
+ }
+
+ @Override
+ public ArrayMap<String, Bundle> getFieldClassificationArgs() {
+ final ArrayMap<String, Bundle> packageArgs = mPackageUserData.getFieldClassificationArgs();
+ final ArrayMap<String, Bundle> genericArgs = mGenericUserData == null ? null :
+ mGenericUserData.getFieldClassificationArgs();
+
+ ArrayMap<String, Bundle> categoryArgs = null;
+ if (packageArgs != null || genericArgs != null) {
+ categoryArgs = new ArrayMap<>();
+ if (genericArgs != null) {
+ categoryArgs.putAll(genericArgs);
+ }
+ if (packageArgs != null) {
+ categoryArgs.putAll(packageArgs);
+ }
+ }
+
+ return categoryArgs;
+ }
+
+ @Override
+ public String[] getCategoryIds() {
+ return mCategories;
+ }
+
+ @Override
+ public String[] getValues() {
+ return mValues;
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ // OK to print UserData because UserData.toString() is PII-aware
+ final StringBuilder builder = new StringBuilder("genericUserData=")
+ .append(mGenericUserData)
+ .append(", packageUserData=").append(mPackageUserData);
+ return builder.toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mGenericUserData, 0);
+ parcel.writeParcelable(mPackageUserData, 0);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<CompositeUserData> CREATOR =
+ new Parcelable.Creator<CompositeUserData>() {
+ @Override
+ public CompositeUserData 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 UserData genericUserData = parcel.readParcelable(null);
+ final UserData packageUserData = parcel.readParcelable(null);
+ return new CompositeUserData(genericUserData, packageUserData);
+ }
+
+ @Override
+ public CompositeUserData[] newArray(int size) {
+ return new CompositeUserData[size];
+ }
+ };
+}
diff --git a/android/service/autofill/CustomDescription.java b/android/service/autofill/CustomDescription.java
new file mode 100644
index 0000000..e274460
--- /dev/null
+++ b/android/service/autofill/CustomDescription.java
@@ -0,0 +1,478 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+
+/**
+ * Defines a custom description for the autofill save UI.
+ *
+ * <p>This is useful when the autofill service needs to show a detailed view of what would be saved;
+ * for example, when the screen contains a credit card, it could display a logo of the credit card
+ * bank, the last four digits of the credit card number, and its expiration number.
+ *
+ * <p>A custom description is made of 2 parts:
+ * <ul>
+ * <li>A {@link RemoteViews presentation template} containing children views.
+ * <li>{@link Transformation Transformations} to populate the children views.
+ * </ul>
+ *
+ * <p>For the credit card example mentioned above, the (simplified) template would be:
+ *
+ * <pre class="prettyprint">
+ * <LinearLayout>
+ * <ImageView android:id="@+id/templateccLogo"/>
+ * <TextView android:id="@+id/templateCcNumber"/>
+ * <TextView android:id="@+id/templateExpDate"/>
+ * </LinearLayout>
+ * </pre>
+ *
+ * <p>Which in code translates to:
+ *
+ * <pre class="prettyprint">
+ * CustomDescription.Builder buider = new Builder(new RemoteViews(pgkName, R.layout.cc_template);
+ * </pre>
+ *
+ * <p>Then the value of each of the 3 children would be changed at runtime based on the the value of
+ * the screen fields and the {@link Transformation Transformations}:
+ *
+ * <pre class="prettyprint">
+ * // Image child - different logo for each bank, based on credit card prefix
+ * builder.addChild(R.id.templateccLogo,
+ * new ImageTransformation.Builder(ccNumberId)
+ * .addOption(Pattern.compile("^4815.*$"), R.drawable.ic_credit_card_logo1)
+ * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2)
+ * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3)
+ * .build();
+ * // Masked credit card number (as .....LAST_4_DIGITS)
+ * builder.addChild(R.id.templateCcNumber, new CharSequenceTransformation
+ * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
+ * .build();
+ * // Expiration date as MM / YYYY:
+ * builder.addChild(R.id.templateExpDate, new CharSequenceTransformation
+ * .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
+ * .addField(ccExpYearId, Pattern.compile("^(\\d\\d)$"), "/$1")
+ * .build();
+ * </pre>
+ *
+ * <p>See {@link ImageTransformation}, {@link CharSequenceTransformation} for more info about these
+ * transformations.
+ */
+public final class CustomDescription implements Parcelable {
+
+ private final RemoteViews mPresentation;
+ private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
+ private final ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates;
+ private final SparseArray<InternalOnClickAction> mActions;
+
+ private CustomDescription(Builder builder) {
+ mPresentation = builder.mPresentation;
+ mTransformations = builder.mTransformations;
+ mUpdates = builder.mUpdates;
+ mActions = builder.mActions;
+ }
+
+ /** @hide */
+ @Nullable
+ public RemoteViews getPresentation() {
+ return mPresentation;
+ }
+
+ /** @hide */
+ @Nullable
+ public ArrayList<Pair<Integer, InternalTransformation>> getTransformations() {
+ return mTransformations;
+ }
+
+ /** @hide */
+ @Nullable
+ public ArrayList<Pair<InternalValidator, BatchUpdates>> getUpdates() {
+ return mUpdates;
+ }
+
+ /** @hide */
+ @Nullable
+ @TestApi
+ public SparseArray<InternalOnClickAction> getActions() {
+ return mActions;
+ }
+
+ /**
+ * Builder for {@link CustomDescription} objects.
+ */
+ public static class Builder {
+ private final RemoteViews mPresentation;
+
+ private boolean mDestroyed;
+ private ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
+ private ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates;
+ private SparseArray<InternalOnClickAction> mActions;
+
+ /**
+ * Default constructor.
+ *
+ * <p><b>Note:</b> If any child view of presentation triggers a
+ * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent) pending intent
+ * on click}, such {@link PendingIntent} must follow the restrictions below, otherwise
+ * it might not be triggered or the autofill save UI might not be shown when its activity
+ * is finished:
+ * <ul>
+ * <li>It cannot be created with the {@link PendingIntent#FLAG_IMMUTABLE} flag.
+ * <li>It must be a PendingIntent for an {@link Activity}.
+ * <li>The activity must call {@link Activity#finish()} when done.
+ * <li>The activity should not launch other activities.
+ * </ul>
+ *
+ * @param parentPresentation template presentation with (optional) children views.
+ * @throws NullPointerException if {@code parentPresentation} is null (on Android
+ * {@link android.os.Build.VERSION_CODES#P} or higher).
+ */
+ public Builder(@NonNull RemoteViews parentPresentation) {
+ mPresentation = Preconditions.checkNotNull(parentPresentation);
+ }
+
+ /**
+ * Adds a transformation to replace the value of a child view with the fields in the
+ * screen.
+ *
+ * <p>When multiple transformations are added for the same child view, they will be applied
+ * in the same order as added.
+ *
+ * @param id view id of the children view.
+ * @param transformation an implementation provided by the Android System.
+ *
+ * @return this builder.
+ *
+ * @throws IllegalArgumentException if {@code transformation} is not a class provided
+ * by the Android System.
+ * @throws IllegalStateException if {@link #build()} was already called.
+ */
+ @NonNull
+ public Builder addChild(int id, @NonNull Transformation transformation) {
+ throwIfDestroyed();
+ Preconditions.checkArgument((transformation instanceof InternalTransformation),
+ "not provided by Android System: " + transformation);
+ if (mTransformations == null) {
+ mTransformations = new ArrayList<>();
+ }
+ mTransformations.add(new Pair<>(id, (InternalTransformation) transformation));
+ return this;
+ }
+
+ /**
+ * Updates the {@link RemoteViews presentation template} when a condition is satisfied by
+ * applying a series of remote view operations. This allows dynamic customization of the
+ * portion of the save UI that is controlled by the autofill service. Such dynamic
+ * customization is based on the content of target views.
+ *
+ * <p>The updates are applied in the sequence they are added, after the
+ * {@link #addChild(int, Transformation) transformations} are applied to the children
+ * views.
+ *
+ * <p>For example, to make children views visible when fields are not empty:
+ *
+ * <pre class="prettyprint">
+ * RemoteViews template = new RemoteViews(pgkName, R.layout.my_full_template);
+ *
+ * Pattern notEmptyPattern = Pattern.compile(".+");
+ * Validator hasAddress = new RegexValidator(addressAutofillId, notEmptyPattern);
+ * Validator hasCcNumber = new RegexValidator(ccNumberAutofillId, notEmptyPattern);
+ *
+ * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_full_template)
+ * addressUpdates.setViewVisibility(R.id.address, View.VISIBLE);
+ *
+ * // Make address visible
+ * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder()
+ * .updateTemplate(addressUpdates)
+ * .build();
+ *
+ * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_full_template)
+ * ccUpdates.setViewVisibility(R.id.cc_number, View.VISIBLE);
+ *
+ * // Mask credit card number (as .....LAST_4_DIGITS) and make it visible
+ * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder()
+ * .updateTemplate(ccUpdates)
+ * .transformChild(R.id.templateCcNumber, new CharSequenceTransformation
+ * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
+ * .build())
+ * .build();
+ *
+ * CustomDescription customDescription = new CustomDescription.Builder(template)
+ * .batchUpdate(hasAddress, addressBatchUpdates)
+ * .batchUpdate(hasCcNumber, ccBatchUpdates)
+ * .build();
+ * </pre>
+ *
+ * <p>Another approach is to add a child first, then apply the transformations. Example:
+ *
+ * <pre class="prettyprint">
+ * RemoteViews template = new RemoteViews(pgkName, R.layout.my_base_template);
+ *
+ * RemoteViews addressPresentation = new RemoteViews(pgkName, R.layout.address)
+ * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_template)
+ * addressUpdates.addView(R.id.parentId, addressPresentation);
+ * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder()
+ * .updateTemplate(addressUpdates)
+ * .build();
+ *
+ * RemoteViews ccPresentation = new RemoteViews(pgkName, R.layout.cc)
+ * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_template)
+ * ccUpdates.addView(R.id.parentId, ccPresentation);
+ * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder()
+ * .updateTemplate(ccUpdates)
+ * .transformChild(R.id.templateCcNumber, new CharSequenceTransformation
+ * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
+ * .build())
+ * .build();
+ *
+ * CustomDescription customDescription = new CustomDescription.Builder(template)
+ * .batchUpdate(hasAddress, addressBatchUpdates)
+ * .batchUpdate(hasCcNumber, ccBatchUpdates)
+ * .build();
+ * </pre>
+ *
+ * @param condition condition used to trigger the updates.
+ * @param updates actions to be applied to the
+ * {@link #Builder(RemoteViews) template presentation} when the condition
+ * is satisfied.
+ *
+ * @return this builder
+ *
+ * @throws IllegalArgumentException if {@code condition} is not a class provided
+ * by the Android System.
+ * @throws IllegalStateException if {@link #build()} was already called.
+ */
+ @NonNull
+ public Builder batchUpdate(@NonNull Validator condition, @NonNull BatchUpdates updates) {
+ throwIfDestroyed();
+ Preconditions.checkArgument((condition instanceof InternalValidator),
+ "not provided by Android System: " + condition);
+ Preconditions.checkNotNull(updates);
+ if (mUpdates == null) {
+ mUpdates = new ArrayList<>();
+ }
+ mUpdates.add(new Pair<>((InternalValidator) condition, updates));
+ return this;
+ }
+
+ /**
+ * Sets an action to be applied to the {@link RemoteViews presentation template} when the
+ * child view with the given {@code id} is clicked.
+ *
+ * <p>Typically used when the presentation uses a masked field (like {@code ****}) for
+ * sensitive fields like passwords or credit cards numbers, but offers a an icon that the
+ * user can tap to show the value for that field.
+ *
+ * <p>Example:
+ *
+ * <pre class="prettyprint">
+ * customDescriptionBuilder
+ * .addChild(R.id.password_plain, new CharSequenceTransformation
+ * .Builder(passwordId, Pattern.compile("^(.*)$"), "$1").build())
+ * .addOnClickAction(R.id.showIcon, new VisibilitySetterAction
+ * .Builder(R.id.hideIcon, View.VISIBLE)
+ * .setVisibility(R.id.showIcon, View.GONE)
+ * .setVisibility(R.id.password_plain, View.VISIBLE)
+ * .setVisibility(R.id.password_masked, View.GONE)
+ * .build())
+ * .addOnClickAction(R.id.hideIcon, new VisibilitySetterAction
+ * .Builder(R.id.showIcon, View.VISIBLE)
+ * .setVisibility(R.id.hideIcon, View.GONE)
+ * .setVisibility(R.id.password_masked, View.VISIBLE)
+ * .setVisibility(R.id.password_plain, View.GONE)
+ * .build());
+ * </pre>
+ *
+ * <p><b>Note:</b> Currently only one action can be applied to a child; if this method
+ * is called multiple times passing the same {@code id}, only the last call will be used.
+ *
+ * @param id resource id of the child view.
+ * @param action action to be performed. Must be an an implementation provided by the
+ * Android System.
+ *
+ * @return this builder
+ *
+ * @throws IllegalArgumentException if {@code action} is not a class provided
+ * by the Android System.
+ * @throws IllegalStateException if {@link #build()} was already called.
+ */
+ @NonNull
+ public Builder addOnClickAction(int id, @NonNull OnClickAction action) {
+ throwIfDestroyed();
+ Preconditions.checkArgument((action instanceof InternalOnClickAction),
+ "not provided by Android System: " + action);
+ if (mActions == null) {
+ mActions = new SparseArray<InternalOnClickAction>();
+ }
+ mActions.put(id, (InternalOnClickAction) action);
+
+ return this;
+ }
+
+ /**
+ * Creates a new {@link CustomDescription} instance.
+ */
+ @NonNull
+ public CustomDescription build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new CustomDescription(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return new StringBuilder("CustomDescription: [presentation=")
+ .append(mPresentation)
+ .append(", transformations=")
+ .append(mTransformations == null ? "N/A" : mTransformations.size())
+ .append(", updates=")
+ .append(mUpdates == null ? "N/A" : mUpdates.size())
+ .append(", actions=")
+ .append(mActions == null ? "N/A" : mActions.size())
+ .append("]").toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mPresentation, flags);
+ if (mPresentation == null) return;
+
+ if (mTransformations == null) {
+ dest.writeIntArray(null);
+ } else {
+ final int size = mTransformations.size();
+ final int[] ids = new int[size];
+ final InternalTransformation[] values = new InternalTransformation[size];
+ for (int i = 0; i < size; i++) {
+ final Pair<Integer, InternalTransformation> pair = mTransformations.get(i);
+ ids[i] = pair.first;
+ values[i] = pair.second;
+ }
+ dest.writeIntArray(ids);
+ dest.writeParcelableArray(values, flags);
+ }
+ if (mUpdates == null) {
+ dest.writeParcelableArray(null, flags);
+ } else {
+ final int size = mUpdates.size();
+ final InternalValidator[] conditions = new InternalValidator[size];
+ final BatchUpdates[] updates = new BatchUpdates[size];
+
+ for (int i = 0; i < size; i++) {
+ final Pair<InternalValidator, BatchUpdates> pair = mUpdates.get(i);
+ conditions[i] = pair.first;
+ updates[i] = pair.second;
+ }
+ dest.writeParcelableArray(conditions, flags);
+ dest.writeParcelableArray(updates, flags);
+ }
+ if (mActions == null) {
+ dest.writeIntArray(null);
+ } else {
+ final int size = mActions.size();
+ final int[] ids = new int[size];
+ final InternalOnClickAction[] values = new InternalOnClickAction[size];
+ for (int i = 0; i < size; i++) {
+ ids[i] = mActions.keyAt(i);
+ values[i] = mActions.valueAt(i);
+ }
+ dest.writeIntArray(ids);
+ dest.writeParcelableArray(values, flags);
+ }
+ }
+ public static final @android.annotation.NonNull Parcelable.Creator<CustomDescription> CREATOR =
+ new Parcelable.Creator<CustomDescription>() {
+ @Override
+ public CustomDescription 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 RemoteViews parentPresentation = parcel.readParcelable(null);
+ if (parentPresentation == null) return null;
+
+ final Builder builder = new Builder(parentPresentation);
+ final int[] transformationIds = parcel.createIntArray();
+ if (transformationIds != null) {
+ final InternalTransformation[] values =
+ parcel.readParcelableArray(null, InternalTransformation.class);
+ final int size = transformationIds.length;
+ for (int i = 0; i < size; i++) {
+ builder.addChild(transformationIds[i], values[i]);
+ }
+ }
+ final InternalValidator[] conditions =
+ parcel.readParcelableArray(null, InternalValidator.class);
+ if (conditions != null) {
+ final BatchUpdates[] updates = parcel.readParcelableArray(null, BatchUpdates.class);
+ final int size = conditions.length;
+ for (int i = 0; i < size; i++) {
+ builder.batchUpdate(conditions[i], updates[i]);
+ }
+ }
+ final int[] actionIds = parcel.createIntArray();
+ if (actionIds != null) {
+ final InternalOnClickAction[] values =
+ parcel.readParcelableArray(null, InternalOnClickAction.class);
+ final int size = actionIds.length;
+ for (int i = 0; i < size; i++) {
+ builder.addOnClickAction(actionIds[i], values[i]);
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ public CustomDescription[] newArray(int size) {
+ return new CustomDescription[size];
+ }
+ };
+}
diff --git a/android/service/autofill/Dataset.java b/android/service/autofill/Dataset.java
new file mode 100644
index 0000000..08aa534
--- /dev/null
+++ b/android/service/autofill/Dataset.java
@@ -0,0 +1,789 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.IntentSender;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * <p>A <code>Dataset</code> object represents a group of fields (key / value pairs) used
+ * to autofill parts of a screen.
+ *
+ * <p>For more information about the role of datasets in the autofill workflow, read
+ * <a href="/guide/topics/text/autofill-services">Build autofill services</a> and the
+ * <code><a href="/reference/android/service/autofill/AutofillService">AutofillService</a></code>
+ * documentation.
+ *
+ * <a name="BasicUsage"></a>
+ * <h3>Basic usage</h3>
+ *
+ * <p>In its simplest form, a dataset contains one or more fields (comprised of
+ * an {@link AutofillId id}, a {@link AutofillValue value}, and an optional filter
+ * {@link Pattern regex}); and one or more {@link RemoteViews presentations} for these fields
+ * (each field could have its own {@link RemoteViews presentation}, or use the default
+ * {@link RemoteViews presentation} associated with the whole dataset).
+ *
+ * <p>When an autofill service returns datasets in a {@link FillResponse}
+ * and the screen input is focused in a view that is present in at least one of these datasets,
+ * the Android System displays a UI containing the {@link RemoteViews presentation} of
+ * all datasets pairs that have that view's {@link AutofillId}. Then, when the user selects a
+ * dataset from the UI, all views in that dataset are autofilled.
+ *
+ * <p>If both the current Input Method and autofill service supports inline suggestions, the Dataset
+ * can be shown by the keyboard as a suggestion. To use this feature, the Dataset should contain
+ * an {@link InlinePresentation} representing how the inline suggestion UI will be rendered.
+ *
+ * <a name="Authentication"></a>
+ * <h3>Dataset authentication</h3>
+ *
+ * <p>In a more sophisticated form, the dataset values can be protected until the user authenticates
+ * the dataset—in that case, when a dataset is selected by the user, the Android System
+ * launches an intent set by the service to "unlock" the dataset.
+ *
+ * <p>For example, when a data set contains credit card information (such as number,
+ * expiration date, and verification code), you could provide a dataset presentation saying
+ * "Tap to authenticate". Then when the user taps that option, you would launch an activity asking
+ * the user to enter the credit card code, and if the user enters a valid code, you could then
+ * "unlock" the dataset.
+ *
+ * <p>You can also use authenticated datasets to offer an interactive UI for the user. For example,
+ * if the activity being autofilled is an account creation screen, you could use an authenticated
+ * dataset to automatically generate a random password for the user.
+ *
+ * <p>See {@link Dataset.Builder#setAuthentication(IntentSender)} for more details about the dataset
+ * authentication mechanism.
+ *
+ * <a name="Filtering"></a>
+ * <h3>Filtering</h3>
+ * <p>The autofill UI automatically changes which values are shown based on value of the view
+ * anchoring it, following the rules below:
+ * <ol>
+ * <li>If the view's {@link android.view.View#getAutofillValue() autofill value} is not
+ * {@link AutofillValue#isText() text} or is empty, all datasets are shown.
+ * <li>Datasets that have a filter regex (set through
+ * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern)} or
+ * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}) and whose
+ * regex matches the view's text value converted to lower case are shown.
+ * <li>Datasets that do not require authentication, have a field value that is
+ * {@link AutofillValue#isText() text} and whose {@link AutofillValue#getTextValue() value} starts
+ * with the lower case value of the view's text are shown.
+ * <li>All other datasets are hidden.
+ * </ol>
+ *
+ */
+public final class Dataset implements Parcelable {
+
+ private final ArrayList<AutofillId> mFieldIds;
+ private final ArrayList<AutofillValue> mFieldValues;
+ private final ArrayList<RemoteViews> mFieldPresentations;
+ private final ArrayList<InlinePresentation> mFieldInlinePresentations;
+ private final ArrayList<DatasetFieldFilter> mFieldFilters;
+ private final RemoteViews mPresentation;
+ @Nullable private final InlinePresentation mInlinePresentation;
+ private final IntentSender mAuthentication;
+ @Nullable String mId;
+
+ private Dataset(Builder builder) {
+ mFieldIds = builder.mFieldIds;
+ mFieldValues = builder.mFieldValues;
+ mFieldPresentations = builder.mFieldPresentations;
+ mFieldInlinePresentations = builder.mFieldInlinePresentations;
+ mFieldFilters = builder.mFieldFilters;
+ mPresentation = builder.mPresentation;
+ mInlinePresentation = builder.mInlinePresentation;
+ mAuthentication = builder.mAuthentication;
+ mId = builder.mId;
+ }
+
+ /** @hide */
+ public @Nullable ArrayList<AutofillId> getFieldIds() {
+ return mFieldIds;
+ }
+
+ /** @hide */
+ public @Nullable ArrayList<AutofillValue> getFieldValues() {
+ return mFieldValues;
+ }
+
+ /** @hide */
+ public RemoteViews getFieldPresentation(int index) {
+ final RemoteViews customPresentation = mFieldPresentations.get(index);
+ return customPresentation != null ? customPresentation : mPresentation;
+ }
+
+ /** @hide */
+ @Nullable
+ public InlinePresentation getFieldInlinePresentation(int index) {
+ final InlinePresentation inlinePresentation = mFieldInlinePresentations.get(index);
+ return inlinePresentation != null ? inlinePresentation : mInlinePresentation;
+ }
+
+ /** @hide */
+ @Nullable
+ public DatasetFieldFilter getFilter(int index) {
+ return mFieldFilters.get(index);
+ }
+
+ /** @hide */
+ public @Nullable IntentSender getAuthentication() {
+ return mAuthentication;
+ }
+
+ /** @hide */
+ public boolean isEmpty() {
+ return mFieldIds == null || mFieldIds.isEmpty();
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ final StringBuilder builder = new StringBuilder("Dataset[");
+ if (mId == null) {
+ builder.append("noId");
+ } else {
+ // Cannot disclose id because it could contain PII.
+ builder.append("id=").append(mId.length()).append("_chars");
+ }
+ if (mFieldIds != null) {
+ builder.append(", fieldIds=").append(mFieldIds);
+ }
+ if (mFieldValues != null) {
+ builder.append(", fieldValues=").append(mFieldValues);
+ }
+ if (mFieldPresentations != null) {
+ builder.append(", fieldPresentations=").append(mFieldPresentations.size());
+ }
+ if (mFieldInlinePresentations != null) {
+ builder.append(", fieldInlinePresentations=").append(mFieldInlinePresentations.size());
+ }
+ if (mFieldFilters != null) {
+ builder.append(", fieldFilters=").append(mFieldFilters.size());
+ }
+ if (mPresentation != null) {
+ builder.append(", hasPresentation");
+ }
+ if (mInlinePresentation != null) {
+ builder.append(", hasInlinePresentation");
+ }
+ if (mAuthentication != null) {
+ builder.append(", hasAuthentication");
+ }
+ return builder.append(']').toString();
+ }
+
+ /**
+ * Gets the id of this dataset.
+ *
+ * @return The id of this dataset or {@code null} if not set
+ *
+ * @hide
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * A builder for {@link Dataset} objects. You must provide at least
+ * one value for a field or set an authentication intent.
+ */
+ public static final class Builder {
+ private ArrayList<AutofillId> mFieldIds;
+ private ArrayList<AutofillValue> mFieldValues;
+ private ArrayList<RemoteViews> mFieldPresentations;
+ private ArrayList<InlinePresentation> mFieldInlinePresentations;
+ private ArrayList<DatasetFieldFilter> mFieldFilters;
+ private RemoteViews mPresentation;
+ @Nullable private InlinePresentation mInlinePresentation;
+ private IntentSender mAuthentication;
+ private boolean mDestroyed;
+ @Nullable private String mId;
+
+ /**
+ * Creates a new builder.
+ *
+ * @param presentation The presentation used to visualize this dataset.
+ */
+ public Builder(@NonNull RemoteViews presentation) {
+ Preconditions.checkNotNull(presentation, "presentation must be non-null");
+ mPresentation = presentation;
+ }
+
+ /**
+ * Creates a new builder.
+ *
+ * <p>Only called by augmented autofill.
+ *
+ * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset
+ * as inline suggestions. If the dataset supports inline suggestions,
+ * this should not be null.
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public Builder(@NonNull InlinePresentation inlinePresentation) {
+ Preconditions.checkNotNull(inlinePresentation, "inlinePresentation must be non-null");
+ mInlinePresentation = inlinePresentation;
+ }
+
+ /**
+ * Creates a new builder for a dataset where each field will be visualized independently.
+ *
+ * <p>When using this constructor, fields must be set through
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} or
+ * {@link #setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Sets the {@link InlinePresentation} used to visualize this dataset as inline suggestions.
+ * If the dataset supports inline suggestions this should not be null.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setInlinePresentation(
+ @NonNull InlinePresentation inlinePresentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(inlinePresentation, "inlinePresentation must be non-null");
+ mInlinePresentation = inlinePresentation;
+ return this;
+ }
+
+ /**
+ * Triggers a custom UI before before autofilling the screen with the contents of this
+ * dataset.
+ *
+ * <p><b>Note:</b> Although the name of this method suggests that it should be used just for
+ * authentication flow, it can be used for other advanced flows; see {@link AutofillService}
+ * for examples.
+ *
+ * <p>This method is called when you need to provide an authentication
+ * UI for the data set. For example, when a data set contains credit card information
+ * (such as number, expiration date, and verification code), you can display UI
+ * asking for the verification code before filing in the data. Even if the
+ * data set is completely populated the system will launch the specified authentication
+ * intent and will need your approval to fill it in. Since the data set is "locked"
+ * until the user authenticates it, typically this data set name is masked
+ * (for example, "VISA....1234"). Typically you would want to store the data set
+ * labels non-encrypted and the actual sensitive data encrypted and not in memory.
+ * This allows showing the labels in the UI while involving the user if one of
+ * the items with these labels is chosen. Note that if you use sensitive data as
+ * a label, for example an email address, then it should also be encrypted.</p>
+ *
+ * <p>When a user triggers autofill, the system launches the provided intent
+ * whose extras will have the {@link
+ * android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen content},
+ * and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE client
+ * state}. Once you complete your authentication flow you should set the activity
+ * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated
+ * {@link Dataset dataset} or a fully-populated {@link FillResponse response} by
+ * setting it to the {@link
+ * android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra. If you
+ * provide a dataset in the result, it will replace the authenticated dataset and
+ * will be immediately filled in. If you provide a response, it will replace the
+ * current response and the UI will be refreshed. For example, if you provided
+ * credit card information without the CVV for the data set in the {@link FillResponse
+ * response} then the returned data set should contain the CVV entry.
+ *
+ * <p><b>Note:</b> Do not make the provided pending intent
+ * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the
+ * platform needs to fill in the authentication arguments.
+ *
+ * @param authentication Intent to an activity with your authentication flow.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ *
+ * @see android.app.PendingIntent
+ */
+ public @NonNull Builder setAuthentication(@Nullable IntentSender authentication) {
+ throwIfDestroyed();
+ mAuthentication = authentication;
+ return this;
+ }
+
+ /**
+ * Sets the id for the dataset so its usage can be tracked.
+ *
+ * <p>Dataset usage can be tracked for 2 purposes:
+ *
+ * <ul>
+ * <li>For statistical purposes, the service can call
+ * {@link AutofillService#getFillEventHistory()} when handling {@link
+ * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}
+ * calls.
+ * <li>For normal autofill workflow, the service can call
+ * {@link SaveRequest#getDatasetIds()} when handling
+ * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} calls.
+ * </ul>
+ *
+ * @param id id for this dataset or {@code null} to unset.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setId(@Nullable String id) {
+ throwIfDestroyed();
+ mId = id;
+ return this;
+ }
+
+ /**
+ * Sets the value of a field.
+ *
+ * <b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, this method would
+ * throw an {@link IllegalStateException} if this builder was constructed without a
+ * {@link RemoteViews presentation}. Android {@link android.os.Build.VERSION_CODES#P} and
+ * higher removed this restriction because datasets used as an
+ * {@link android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT
+ * authentication result} do not need a presentation. But if you don't set the presentation
+ * in the constructor in a dataset that is meant to be shown to the user, the autofill UI
+ * for this field will not be displayed.
+ *
+ * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and
+ * higher, datasets that require authentication can be also be filtered by passing a
+ * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value) {
+ throwIfDestroyed();
+ setLifeTheUniverseAndEverything(id, value, null, null, null);
+ return this;
+ }
+
+ /**
+ * Sets the value of a field, using a custom {@link RemoteViews presentation} to
+ * visualize it.
+ *
+ * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and
+ * higher, datasets that require authentication can be also be filtered by passing a
+ * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter.
+ *
+ * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color
+ * or background color: Autofill on different platforms may have different themes.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param presentation the presentation used to visualize this field.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
+ @NonNull RemoteViews presentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(presentation, "presentation cannot be null");
+ setLifeTheUniverseAndEverything(id, value, presentation, null, null);
+ return this;
+ }
+
+ /**
+ * Sets the value of a field using an <a href="#Filtering">explicit filter</a>.
+ *
+ * <p>This method is typically used when the dataset requires authentication and the service
+ * does not know its value but wants to hide the dataset after the user enters a minimum
+ * number of characters. For example, if the dataset represents a credit card number and the
+ * service does not want to show the "Tap to authenticate" message until the user tapped
+ * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}.
+ *
+ * <p><b>Note:</b> If the dataset requires authentication but the service knows its text
+ * value it's easier to filter by calling {@link #setValue(AutofillId, AutofillValue)} and
+ * use the value to filter.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param filter regex used to determine if the dataset should be shown in the autofill UI;
+ * when {@code null}, it disables filtering on that dataset (this is the recommended
+ * approach when {@code value} is not {@code null} and field contains sensitive data
+ * such as passwords).
+ *
+ * @return this builder.
+ * @throws IllegalStateException if the builder was constructed without a
+ * {@link RemoteViews presentation} or {@link #build()} was already called.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
+ @Nullable Pattern filter) {
+ throwIfDestroyed();
+ Preconditions.checkState(mPresentation != null,
+ "Dataset presentation not set on constructor");
+ setLifeTheUniverseAndEverything(id, value, null, null, new DatasetFieldFilter(filter));
+ return this;
+ }
+
+ /**
+ * Sets the value of a field, using a custom {@link RemoteViews presentation} to
+ * visualize it and a <a href="#Filtering">explicit filter</a>.
+ *
+ * <p>This method is typically used when the dataset requires authentication and the service
+ * does not know its value but wants to hide the dataset after the user enters a minimum
+ * number of characters. For example, if the dataset represents a credit card number and the
+ * service does not want to show the "Tap to authenticate" message until the user tapped
+ * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}.
+ *
+ * <p><b>Note:</b> If the dataset requires authentication but the service knows its text
+ * value it's easier to filter by calling
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param filter regex used to determine if the dataset should be shown in the autofill UI;
+ * when {@code null}, it disables filtering on that dataset (this is the recommended
+ * approach when {@code value} is not {@code null} and field contains sensitive data
+ * such as passwords).
+ * @param presentation the presentation used to visualize this field.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
+ @Nullable Pattern filter, @NonNull RemoteViews presentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(presentation, "presentation cannot be null");
+ setLifeTheUniverseAndEverything(id, value, presentation, null,
+ new DatasetFieldFilter(filter));
+ return this;
+ }
+
+ /**
+ * Sets the value of a field, using a custom {@link RemoteViews presentation} to
+ * visualize it and an {@link InlinePresentation} to visualize it as an inline suggestion.
+ *
+ * <p><b>Note:</b> If the dataset requires authentication but the service knows its text
+ * value it's easier to filter by calling
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param presentation the presentation used to visualize this field.
+ * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset
+ * as inline suggestions. If the dataset supports inline suggestions,
+ * this should not be null.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
+ @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(presentation, "presentation cannot be null");
+ Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null");
+ setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, null);
+ return this;
+ }
+
+ /**
+ * Sets the value of a field, using a custom {@link RemoteViews presentation} to
+ * visualize it and a <a href="#Filtering">explicit filter</a>, and an
+ * {@link InlinePresentation} to visualize it as an inline suggestion.
+ *
+ * <p>This method is typically used when the dataset requires authentication and the service
+ * does not know its value but wants to hide the dataset after the user enters a minimum
+ * number of characters. For example, if the dataset represents a credit card number and the
+ * service does not want to show the "Tap to authenticate" message until the user tapped
+ * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}.
+ *
+ * <p><b>Note:</b> If the dataset requires authentication but the service knows its text
+ * value it's easier to filter by calling
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param filter regex used to determine if the dataset should be shown in the autofill UI;
+ * when {@code null}, it disables filtering on that dataset (this is the recommended
+ * approach when {@code value} is not {@code null} and field contains sensitive data
+ * such as passwords).
+ * @param presentation the presentation used to visualize this field.
+ * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset
+ * as inline suggestions. If the dataset supports inline suggestions, this
+ * should not be null.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
+ @Nullable Pattern filter, @NonNull RemoteViews presentation,
+ @NonNull InlinePresentation inlinePresentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(presentation, "presentation cannot be null");
+ Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null");
+ setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation,
+ new DatasetFieldFilter(filter));
+ return this;
+ }
+
+ /**
+ * Sets the value of a field with an <a href="#Filtering">explicit filter</a>, and using an
+ * {@link InlinePresentation} to visualize it as an inline suggestion.
+ *
+ * <p>Only called by augmented autofill.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param filter regex used to determine if the dataset should be shown in the autofill UI;
+ * when {@code null}, it disables filtering on that dataset (this is the recommended
+ * approach when {@code value} is not {@code null} and field contains sensitive data
+ * such as passwords).
+ * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset
+ * as inline suggestions. If the dataset supports inline suggestions, this
+ * should not be null.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public @NonNull Builder setFieldInlinePresentation(@NonNull AutofillId id,
+ @Nullable AutofillValue value, @Nullable Pattern filter,
+ @NonNull InlinePresentation inlinePresentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null");
+ setLifeTheUniverseAndEverything(id, value, null, inlinePresentation,
+ new DatasetFieldFilter(filter));
+ return this;
+ }
+
+ private void setLifeTheUniverseAndEverything(@NonNull AutofillId id,
+ @Nullable AutofillValue value, @Nullable RemoteViews presentation,
+ @Nullable InlinePresentation inlinePresentation,
+ @Nullable DatasetFieldFilter filter) {
+ Preconditions.checkNotNull(id, "id cannot be null");
+ if (mFieldIds != null) {
+ final int existingIdx = mFieldIds.indexOf(id);
+ if (existingIdx >= 0) {
+ mFieldValues.set(existingIdx, value);
+ mFieldPresentations.set(existingIdx, presentation);
+ mFieldInlinePresentations.set(existingIdx, inlinePresentation);
+ mFieldFilters.set(existingIdx, filter);
+ return;
+ }
+ } else {
+ mFieldIds = new ArrayList<>();
+ mFieldValues = new ArrayList<>();
+ mFieldPresentations = new ArrayList<>();
+ mFieldInlinePresentations = new ArrayList<>();
+ mFieldFilters = new ArrayList<>();
+ }
+ mFieldIds.add(id);
+ mFieldValues.add(value);
+ mFieldPresentations.add(presentation);
+ mFieldInlinePresentations.add(inlinePresentation);
+ mFieldFilters.add(filter);
+ }
+
+ /**
+ * Creates a new {@link Dataset} instance.
+ *
+ * <p>You should not interact with this builder once this method is called.
+ *
+ * @throws IllegalStateException if no field was set (through
+ * {@link #setValue(AutofillId, AutofillValue)} or
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} or
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews, InlinePresentation)}),
+ * or if {@link #build()} was already called.
+ *
+ * @return The built dataset.
+ */
+ public @NonNull Dataset build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ if (mFieldIds == null) {
+ throw new IllegalStateException("at least one value must be set");
+ }
+ return new Dataset(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mPresentation, flags);
+ parcel.writeParcelable(mInlinePresentation, flags);
+ parcel.writeTypedList(mFieldIds, flags);
+ parcel.writeTypedList(mFieldValues, flags);
+ parcel.writeTypedList(mFieldPresentations, flags);
+ parcel.writeTypedList(mFieldInlinePresentations, flags);
+ parcel.writeTypedList(mFieldFilters, flags);
+ parcel.writeParcelable(mAuthentication, flags);
+ parcel.writeString(mId);
+ }
+
+ public static final @NonNull Creator<Dataset> CREATOR = new Creator<Dataset>() {
+ @Override
+ public Dataset 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 RemoteViews presentation = parcel.readParcelable(null);
+ final InlinePresentation inlinePresentation = parcel.readParcelable(null);
+ final Builder builder = presentation != null
+ ? inlinePresentation == null
+ ? new Builder(presentation)
+ : new Builder(presentation).setInlinePresentation(inlinePresentation)
+ : inlinePresentation == null
+ ? new Builder()
+ : new Builder(inlinePresentation);
+ final ArrayList<AutofillId> ids =
+ parcel.createTypedArrayList(AutofillId.CREATOR);
+ final ArrayList<AutofillValue> values =
+ parcel.createTypedArrayList(AutofillValue.CREATOR);
+ final ArrayList<RemoteViews> presentations =
+ parcel.createTypedArrayList(RemoteViews.CREATOR);
+ final ArrayList<InlinePresentation> inlinePresentations =
+ parcel.createTypedArrayList(InlinePresentation.CREATOR);
+ final ArrayList<DatasetFieldFilter> filters =
+ parcel.createTypedArrayList(DatasetFieldFilter.CREATOR);
+ final int inlinePresentationsSize = inlinePresentations.size();
+ for (int i = 0; i < ids.size(); i++) {
+ final AutofillId id = ids.get(i);
+ final AutofillValue value = values.get(i);
+ final RemoteViews fieldPresentation = presentations.get(i);
+ final InlinePresentation fieldInlinePresentation =
+ i < inlinePresentationsSize ? inlinePresentations.get(i) : null;
+ final DatasetFieldFilter filter = filters.get(i);
+ builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation,
+ fieldInlinePresentation, filter);
+ }
+ builder.setAuthentication(parcel.readParcelable(null));
+ builder.setId(parcel.readString());
+ return builder.build();
+ }
+
+ @Override
+ public Dataset[] newArray(int size) {
+ return new Dataset[size];
+ }
+ };
+
+ /**
+ * Helper class used to indicate when the service explicitly set a {@link Pattern} filter for a
+ * dataset field‐ we cannot use a {@link Pattern} directly because then we wouldn't be
+ * able to differentiate whether the service explicitly passed a {@code null} filter to disable
+ * filter, or when it called the methods that does not take a filter {@link Pattern}.
+ *
+ * @hide
+ */
+ public static final class DatasetFieldFilter implements Parcelable {
+
+ @Nullable
+ public final Pattern pattern;
+
+ private DatasetFieldFilter(@Nullable Pattern pattern) {
+ this.pattern = pattern;
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ // Cannot log pattern because it could contain PII
+ return pattern == null ? "null" : pattern.pattern().length() + "_chars";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeSerializable(pattern);
+ }
+
+ @SuppressWarnings("hiding")
+ public static final @android.annotation.NonNull Creator<DatasetFieldFilter> CREATOR =
+ new Creator<DatasetFieldFilter>() {
+
+ @Override
+ public DatasetFieldFilter createFromParcel(Parcel parcel) {
+ return new DatasetFieldFilter((Pattern) parcel.readSerializable());
+ }
+
+ @Override
+ public DatasetFieldFilter[] newArray(int size) {
+ return new DatasetFieldFilter[size];
+ }
+ };
+ }
+}
diff --git a/android/service/autofill/DateTransformation.java b/android/service/autofill/DateTransformation.java
new file mode 100644
index 0000000..338ba74
--- /dev/null
+++ b/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.icu.text.DateFormat;
+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.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 @android.annotation.NonNull 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/android/service/autofill/DateValueSanitizer.java b/android/service/autofill/DateValueSanitizer.java
new file mode 100644
index 0000000..707bab1
--- /dev/null
+++ b/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.icu.text.DateFormat;
+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.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 @android.annotation.NonNull 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/android/service/autofill/FieldClassification.java b/android/service/autofill/FieldClassification.java
new file mode 100644
index 0000000..5bf56cb
--- /dev/null
+++ b/android/service/autofill/FieldClassification.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.view.autofill.Helper;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Represents the <a href="AutofillService.html#FieldClassification">field classification</a>
+ * results for a given field.
+ */
+public final class FieldClassification {
+
+ private final ArrayList<Match> mMatches;
+
+ /** @hide */
+ public FieldClassification(@NonNull ArrayList<Match> matches) {
+ mMatches = Preconditions.checkNotNull(matches);
+ Collections.sort(mMatches, new Comparator<Match>() {
+ @Override
+ public int compare(Match o1, Match o2) {
+ if (o1.mScore > o2.mScore) return -1;
+ if (o1.mScore < o2.mScore) return 1;
+ return 0;
+ }}
+ );
+ }
+
+ /**
+ * Gets the {@link Match matches} with the highest {@link Match#getScore() scores} (sorted in
+ * descending order).
+ *
+ * <p><b>Note:</b> There's no guarantee of how many matches will be returned. In fact,
+ * the Android System might return just the top match to minimize the impact of field
+ * classification in the device's health.
+ */
+ @NonNull
+ public List<Match> getMatches() {
+ return mMatches;
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "FieldClassification: " + mMatches;
+ }
+
+ private void writeToParcel(Parcel parcel) {
+ parcel.writeInt(mMatches.size());
+ for (int i = 0; i < mMatches.size(); i++) {
+ mMatches.get(i).writeToParcel(parcel);
+ }
+ }
+
+ private static FieldClassification readFromParcel(Parcel parcel) {
+ final int size = parcel.readInt();
+ final ArrayList<Match> matches = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ matches.add(i, Match.readFromParcel(parcel));
+ }
+
+ return new FieldClassification(matches);
+ }
+
+ static FieldClassification[] readArrayFromParcel(Parcel parcel) {
+ final int length = parcel.readInt();
+ final FieldClassification[] fcs = new FieldClassification[length];
+ for (int i = 0; i < length; i++) {
+ fcs[i] = readFromParcel(parcel);
+ }
+ return fcs;
+ }
+
+ static void writeArrayToParcel(@NonNull Parcel parcel, @NonNull FieldClassification[] fcs) {
+ parcel.writeInt(fcs.length);
+ for (int i = 0; i < fcs.length; i++) {
+ fcs[i].writeToParcel(parcel);
+ }
+ }
+
+ /**
+ * Represents the score of a {@link UserData} entry for the field.
+ */
+ public static final class Match {
+
+ private final String mCategoryId;
+ private final float mScore;
+
+ /** @hide */
+ public Match(String categoryId, float score) {
+ mCategoryId = Preconditions.checkNotNull(categoryId);
+ mScore = score;
+ }
+
+ /**
+ * Gets the category id of the {@link UserData} entry.
+ */
+ @NonNull
+ public String getCategoryId() {
+ return mCategoryId;
+ }
+
+ /**
+ * Gets a classification score for the value of this field compared to the value of the
+ * {@link UserData} entry.
+ *
+ * <p>The score is based in a comparison of the field value and the user data entry, and it
+ * ranges from {@code 0.0F} to {@code 1.0F}:
+ * <ul>
+ * <li>{@code 1.0F} represents a full match ({@code 100%}).
+ * <li>{@code 0.0F} represents a full mismatch ({@code 0%}).
+ * <li>Any other value is a partial match.
+ * </ul>
+ *
+ * <p>How the score is calculated depends on the
+ * {@link UserData.Builder#setFieldClassificationAlgorithm(String, android.os.Bundle)
+ * algorithm} used.
+ */
+ public float getScore() {
+ return mScore;
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ final StringBuilder string = new StringBuilder("Match: categoryId=");
+ Helper.appendRedacted(string, mCategoryId);
+ return string.append(", score=").append(mScore).toString();
+ }
+
+ private void writeToParcel(@NonNull Parcel parcel) {
+ parcel.writeString(mCategoryId);
+ parcel.writeFloat(mScore);
+ }
+
+ private static Match readFromParcel(@NonNull Parcel parcel) {
+ return new Match(parcel.readString(), parcel.readFloat());
+ }
+ }
+}
diff --git a/android/service/autofill/FieldClassificationUserData.java b/android/service/autofill/FieldClassificationUserData.java
new file mode 100644
index 0000000..3d6cac4
--- /dev/null
+++ b/android/service/autofill/FieldClassificationUserData.java
@@ -0,0 +1,64 @@
+/*
+ * 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 android.os.Bundle;
+import android.util.ArrayMap;
+
+/**
+ * Class used to define a generic UserData for field classification
+ *
+ * @hide
+ */
+public interface FieldClassificationUserData {
+ /**
+ * Gets the name of the default algorithm that is used to calculate
+ * {@link FieldClassification.Match#getScore()} match scores}.
+ */
+ String getFieldClassificationAlgorithm();
+
+ /**
+ * Gets the default field classification args.
+ */
+ Bundle getDefaultFieldClassificationArgs();
+
+ /**
+ * Gets the name of the field classification algorithm for a specific category.
+ *
+ * @param categoryId id of the specific category.
+ */
+ String getFieldClassificationAlgorithmForCategory(String categoryId);
+
+ /**
+ * Gets all field classification algorithms for specific categories.
+ */
+ ArrayMap<String, String> getFieldClassificationAlgorithms();
+
+ /**
+ * Gets all field classification args for specific categories.
+ */
+ ArrayMap<String, Bundle> getFieldClassificationArgs();
+
+ /**
+ * Gets all category ids
+ */
+ String[] getCategoryIds();
+
+ /**
+ * Gets all string values for field classification
+ */
+ String[] getValues();
+}
diff --git a/android/service/autofill/FillCallback.java b/android/service/autofill/FillCallback.java
new file mode 100644
index 0000000..06e2896
--- /dev/null
+++ b/android/service/autofill/FillCallback.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * <p><code>FillCallback</code> handles autofill requests from the {@link AutofillService} into
+ * the {@link Activity} being autofilled.
+ *
+ * <p>To learn about using Autofill services in your app, read
+ * <a href="/guide/topics/text/autofill-services">Build autofill services</a>.
+ */
+public final class FillCallback {
+
+ private static final String TAG = "FillCallback";
+
+ private final IFillCallback mCallback;
+ private final int mRequestId;
+ private boolean mCalled;
+
+ /** @hide */
+ public FillCallback(IFillCallback callback, int requestId) {
+ mCallback = callback;
+ mRequestId = requestId;
+ }
+
+ /**
+ * Notifies the Android System that a fill request
+ * ({@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
+ * FillCallback)}) was successfully fulfilled by the service.
+ *
+ * <p>This method should always be called, even if the service doesn't have the heuristics to
+ * fulfill the request (in which case it should be called with {@code null}).
+ *
+ * <p>See the main {@link AutofillService} documentation for more details and examples.
+ *
+ * @param response autofill information for that activity, or {@code null} when the service
+ * cannot autofill the activity.
+ *
+ * @throws IllegalStateException if this method or {@link #onFailure(CharSequence)} was already
+ * called.
+ */
+ public void onSuccess(@Nullable FillResponse response) {
+ assertNotCalled();
+ mCalled = true;
+
+ if (response != null) {
+ response.setRequestId(mRequestId);
+ }
+
+ try {
+ mCallback.onSuccess(response);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
+ * Notifies the Android System that a fill request (
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
+ * FillCallback)}) could not be fulfilled by the service (for example, because the user data was
+ * not available yet), so the request could be retried later.
+ *
+ * <p><b>Note: </b>this method should not be used when the service didn't have the heursitics to
+ * fulfill the request; in this case, the service should call {@link #onSuccess(FillResponse)
+ * onSuccess(null)} instead.
+ *
+ * <p><b>Note: </b>prior to {@link android.os.Build.VERSION_CODES#Q}, this
+ * method was not working as intended and the service should always call
+ * {@link #onSuccess(FillResponse) onSuccess(null)} instead.
+ *
+ * <p><b>Note: </b>for apps targeting {@link android.os.Build.VERSION_CODES#Q} or higher, this
+ * method just logs the message on {@code logcat}; for apps targetting older SDKs, it also
+ * displays the message to user using a {@link android.widget.Toast}. Generally speaking, you
+ * should not display an error to the user if the request failed, unless the request had the
+ * {@link FillRequest#FLAG_MANUAL_REQUEST} flag.
+ *
+ * @param message error message. <b>Note: </b> this message should <b>not</b> contain PII
+ * (Personally Identifiable Information, such as username or email address).
+ *
+ * @throws IllegalStateException if this method or {@link #onSuccess(FillResponse)} was already
+ * called.
+ */
+ public void onFailure(@Nullable CharSequence message) {
+ Log.w(TAG, "onFailure(): " + message);
+ assertNotCalled();
+ mCalled = true;
+ try {
+ mCallback.onFailure(mRequestId, message);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ private void assertNotCalled() {
+ if (mCalled) {
+ throw new IllegalStateException("Already called");
+ }
+ }
+}
diff --git a/android/service/autofill/FillContext.java b/android/service/autofill/FillContext.java
new file mode 100644
index 0000000..8331550
--- /dev/null
+++ b/android/service/autofill/FillContext.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.SparseIntArray;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.util.DataClass;
+
+import java.util.LinkedList;
+
+/**
+ * This class represents a context for each fill request made via {@link
+ * AutofillService#onFillRequest(FillRequest, CancellationSignal, FillCallback)}.
+ * It contains a snapshot of the UI state, the view ids that were returned by
+ * the {@link AutofillService autofill service} as both required to trigger a save
+ * and optional that can be saved, and the id of the corresponding {@link
+ * FillRequest}.
+ * <p>
+ * This context allows you to inspect the values for the interesting views
+ * in the context they appeared. Also a reference to the corresponding fill
+ * request is useful to store meta-data in the client state bundle passed
+ * to {@link FillResponse.Builder#setClientState(Bundle)} to avoid interpreting
+ * the UI state again while saving.
+ */
+@DataClass(
+ genHiddenConstructor = true,
+ genAidl = false)
+public final class FillContext implements Parcelable {
+
+ /**
+ * The id of the {@link FillRequest fill request} this context
+ * corresponds to. This is useful to associate your custom client
+ * state with every request to avoid reinterpreting the UI when saving
+ * user data.
+ */
+ private final int mRequestId;
+
+ /**
+ * The screen content.
+ */
+ private final @NonNull AssistStructure mStructure;
+
+ /**
+ * The AutofillId of the view that triggered autofill.
+ */
+ private final @NonNull AutofillId mFocusedId;
+
+ /**
+ * Lookup table AutofillId->ViewNode to speed up {@link #findViewNodesByAutofillIds}
+ * This is purely a cache and can be deleted at any time
+ */
+ private transient @Nullable ArrayMap<AutofillId, AssistStructure.ViewNode> mViewNodeLookupTable;
+
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "FillContext [reqId=" + mRequestId + ", focusedId=" + mFocusedId + "]";
+ }
+
+ /**
+ * Finds {@link ViewNode ViewNodes} that have the requested ids.
+ *
+ * @param ids The ids of the node to find.
+ *
+ * @return The nodes indexed in the same way as the ids.
+ *
+ * @hide
+ */
+ @NonNull public ViewNode[] findViewNodesByAutofillIds(@NonNull AutofillId[] ids) {
+ final LinkedList<ViewNode> nodesToProcess = new LinkedList<>();
+ final ViewNode[] foundNodes = new AssistStructure.ViewNode[ids.length];
+
+ // Indexes of foundNodes that are not found yet
+ final SparseIntArray missingNodeIndexes = new SparseIntArray(ids.length);
+
+ for (int i = 0; i < ids.length; i++) {
+ if (mViewNodeLookupTable != null) {
+ int lookupTableIndex = mViewNodeLookupTable.indexOfKey(ids[i]);
+
+ if (lookupTableIndex >= 0) {
+ foundNodes[i] = mViewNodeLookupTable.valueAt(lookupTableIndex);
+ } else {
+ missingNodeIndexes.put(i, /* ignored */ 0);
+ }
+ } else {
+ missingNodeIndexes.put(i, /* ignored */ 0);
+ }
+ }
+
+ final int numWindowNodes = mStructure.getWindowNodeCount();
+ for (int i = 0; i < numWindowNodes; i++) {
+ nodesToProcess.add(mStructure.getWindowNodeAt(i).getRootViewNode());
+ }
+
+ while (missingNodeIndexes.size() > 0 && !nodesToProcess.isEmpty()) {
+ final ViewNode node = nodesToProcess.removeFirst();
+
+ for (int i = 0; i < missingNodeIndexes.size(); i++) {
+ final int index = missingNodeIndexes.keyAt(i);
+ final AutofillId id = ids[index];
+
+ if (id.equals(node.getAutofillId())) {
+ foundNodes[index] = node;
+
+ if (mViewNodeLookupTable == null) {
+ mViewNodeLookupTable = new ArrayMap<>(ids.length);
+ }
+
+ mViewNodeLookupTable.put(id, node);
+
+ missingNodeIndexes.removeAt(i);
+ break;
+ }
+ }
+
+ for (int i = 0; i < node.getChildCount(); i++) {
+ nodesToProcess.addLast(node.getChildAt(i));
+ }
+ }
+
+ // Remember which ids could not be resolved to not search for them again the next time
+ for (int i = 0; i < missingNodeIndexes.size(); i++) {
+ if (mViewNodeLookupTable == null) {
+ mViewNodeLookupTable = new ArrayMap<>(missingNodeIndexes.size());
+ }
+
+ mViewNodeLookupTable.put(ids[missingNodeIndexes.keyAt(i)], null);
+ }
+
+ return foundNodes;
+ }
+
+
+
+ // Code below generated by codegen v1.0.0.
+ //
+ // DO NOT MODIFY!
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/FillContext.java
+ //
+ // CHECKSTYLE:OFF Generated code
+
+ /**
+ * Creates a new FillContext.
+ *
+ * @param requestId
+ * The id of the {@link FillRequest fill request} this context
+ * corresponds to. This is useful to associate your custom client
+ * state with every request to avoid reinterpreting the UI when saving
+ * user data.
+ * @param structure
+ * The screen content.
+ * @param focusedId
+ * The AutofillId of the view that triggered autofill.
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public FillContext(
+ int requestId,
+ @NonNull AssistStructure structure,
+ @NonNull AutofillId focusedId) {
+ this.mRequestId = requestId;
+ this.mStructure = structure;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mStructure);
+ this.mFocusedId = focusedId;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mFocusedId);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * The id of the {@link FillRequest fill request} this context
+ * corresponds to. This is useful to associate your custom client
+ * state with every request to avoid reinterpreting the UI when saving
+ * user data.
+ */
+ @DataClass.Generated.Member
+ public int getRequestId() {
+ return mRequestId;
+ }
+
+ /**
+ * The screen content.
+ */
+ @DataClass.Generated.Member
+ public @NonNull AssistStructure getStructure() {
+ return mStructure;
+ }
+
+ /**
+ * The AutofillId of the view that triggered autofill.
+ */
+ @DataClass.Generated.Member
+ public @NonNull AutofillId getFocusedId() {
+ return mFocusedId;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeInt(mRequestId);
+ dest.writeTypedObject(mStructure, flags);
+ dest.writeTypedObject(mFocusedId, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<FillContext> CREATOR
+ = new Parcelable.Creator<FillContext>() {
+ @Override
+ public FillContext[] newArray(int size) {
+ return new FillContext[size];
+ }
+
+ @Override
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ public FillContext createFromParcel(Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ int requestId = in.readInt();
+ AssistStructure structure = (AssistStructure) in.readTypedObject(AssistStructure.CREATOR);
+ AutofillId focusedId = (AutofillId) in.readTypedObject(AutofillId.CREATOR);
+ return new FillContext(
+ requestId,
+ structure,
+ focusedId);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1565152135263L,
+ codegenVersion = "1.0.0",
+ sourceFile = "frameworks/base/core/java/android/service/autofill/FillContext.java",
+ inputSignatures = "private final int mRequestId\nprivate final @android.annotation.NonNull android.app.assist.AssistStructure mStructure\nprivate final @android.annotation.NonNull android.view.autofill.AutofillId mFocusedId\nprivate transient @android.annotation.Nullable android.util.ArrayMap<android.view.autofill.AutofillId,android.app.assist.AssistStructure.ViewNode> mViewNodeLookupTable\npublic @java.lang.Override java.lang.String toString()\npublic @android.annotation.NonNull android.app.assist.AssistStructure.ViewNode[] findViewNodesByAutofillIds(android.view.autofill.AutofillId[])\nclass FillContext extends java.lang.Object implements [android.os.Parcelable]\[email protected](genHiddenConstructor=true, genAidl=false)")
+ @Deprecated
+ private void __metadata() {}
+
+}
diff --git a/android/service/autofill/FillEventHistory.java b/android/service/autofill/FillEventHistory.java
new file mode 100644
index 0000000..1cd2d62
--- /dev/null
+++ b/android/service/autofill/FillEventHistory.java
@@ -0,0 +1,580 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sVerbose;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Describes what happened after the last
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}
+ * call.
+ *
+ * <p>This history is typically used to keep track of previous user actions to optimize further
+ * requests. For example, the service might return email addresses in alphabetical order by
+ * default, but change that order based on the address the user picked on previous requests.
+ *
+ * <p>The history is not persisted over reboots, and it's cleared every time the service
+ * replies to a
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}
+ * by calling {@link FillCallback#onSuccess(FillResponse)} or
+ * {@link FillCallback#onFailure(CharSequence)} (if the service doesn't call any of these methods,
+ * the history will clear out after some pre-defined time).
+ */
+public final class FillEventHistory implements Parcelable {
+ private static final String TAG = "FillEventHistory";
+
+ /**
+ * Not in parcel. The ID of the autofill session that created the {@link FillResponse}.
+ */
+ private final int mSessionId;
+
+ @Nullable private final Bundle mClientState;
+ @Nullable List<Event> mEvents;
+
+ /** @hide */
+ public int getSessionId() {
+ return mSessionId;
+ }
+
+ /**
+ * Returns the client state set in the previous {@link FillResponse}.
+ *
+ * <p><b>Note: </b>the state is associated with the app that was autofilled in the previous
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}
+ * , which is not necessary the same app being autofilled now.
+ *
+ * @deprecated use {@link #getEvents()} then {@link Event#getClientState()} instead.
+ */
+ @Deprecated
+ @Nullable public Bundle getClientState() {
+ return mClientState;
+ }
+
+ /**
+ * Returns the events occurred after the latest call to
+ * {@link FillCallback#onSuccess(FillResponse)}.
+ *
+ * @return The list of events or {@code null} if non occurred.
+ */
+ @Nullable public List<Event> getEvents() {
+ return mEvents;
+ }
+
+ /**
+ * @hide
+ */
+ public void addEvent(Event event) {
+ if (mEvents == null) {
+ mEvents = new ArrayList<>(1);
+ }
+ mEvents.add(event);
+ }
+
+ /**
+ * @hide
+ */
+ public FillEventHistory(int sessionId, @Nullable Bundle clientState) {
+ mClientState = clientState;
+ mSessionId = sessionId;
+ }
+
+ @Override
+ public String toString() {
+ return mEvents == null ? "no events" : mEvents.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeBundle(mClientState);
+ if (mEvents == null) {
+ parcel.writeInt(0);
+ } else {
+ parcel.writeInt(mEvents.size());
+
+ int numEvents = mEvents.size();
+ for (int i = 0; i < numEvents; i++) {
+ Event event = mEvents.get(i);
+ parcel.writeInt(event.mEventType);
+ parcel.writeString(event.mDatasetId);
+ parcel.writeBundle(event.mClientState);
+ parcel.writeStringList(event.mSelectedDatasetIds);
+ parcel.writeArraySet(event.mIgnoredDatasetIds);
+ parcel.writeTypedList(event.mChangedFieldIds);
+ parcel.writeStringList(event.mChangedDatasetIds);
+
+ parcel.writeTypedList(event.mManuallyFilledFieldIds);
+ if (event.mManuallyFilledFieldIds != null) {
+ final int size = event.mManuallyFilledFieldIds.size();
+ for (int j = 0; j < size; j++) {
+ parcel.writeStringList(event.mManuallyFilledDatasetIds.get(j));
+ }
+ }
+ final AutofillId[] detectedFields = event.mDetectedFieldIds;
+ parcel.writeParcelableArray(detectedFields, flags);
+ if (detectedFields != null) {
+ FieldClassification.writeArrayToParcel(parcel,
+ event.mDetectedFieldClassifications);
+ }
+ }
+ }
+ }
+
+ /**
+ * Description of an event that occured after the latest call to
+ * {@link FillCallback#onSuccess(FillResponse)}.
+ */
+ public static final class Event {
+ /**
+ * A dataset was selected. The dataset selected can be read from {@link #getDatasetId()}.
+ *
+ * <p><b>Note: </b>on Android {@link android.os.Build.VERSION_CODES#O}, this event was also
+ * incorrectly reported after a
+ * {@link Dataset.Builder#setAuthentication(IntentSender) dataset authentication} was
+ * selected and the service returned a dataset in the
+ * {@link AutofillManager#EXTRA_AUTHENTICATION_RESULT} of the activity launched from that
+ * {@link IntentSender}. This behavior was fixed on Android
+ * {@link android.os.Build.VERSION_CODES#O_MR1}.
+ */
+ public static final int TYPE_DATASET_SELECTED = 0;
+
+ /**
+ * A {@link Dataset.Builder#setAuthentication(IntentSender) dataset authentication} was
+ * selected. The dataset authenticated can be read from {@link #getDatasetId()}.
+ */
+ public static final int TYPE_DATASET_AUTHENTICATION_SELECTED = 1;
+
+ /**
+ * A {@link FillResponse.Builder#setAuthentication(android.view.autofill.AutofillId[],
+ * IntentSender, android.widget.RemoteViews) fill response authentication} was selected.
+ */
+ public static final int TYPE_AUTHENTICATION_SELECTED = 2;
+
+ /** A save UI was shown. */
+ public static final int TYPE_SAVE_SHOWN = 3;
+
+ /**
+ * A committed autofill context for which the autofill service provided datasets.
+ *
+ * <p>This event is useful to track:
+ * <ul>
+ * <li>Which datasets (if any) were selected by the user
+ * ({@link #getSelectedDatasetIds()}).
+ * <li>Which datasets (if any) were NOT selected by the user
+ * ({@link #getIgnoredDatasetIds()}).
+ * <li>Which fields in the selected datasets were changed by the user after the dataset
+ * was selected ({@link #getChangedFields()}.
+ * <li>Which fields match the {@link UserData} set by the service.
+ * </ul>
+ *
+ * <p><b>Note: </b>This event is only generated when:
+ * <ul>
+ * <li>The autofill context is committed.
+ * <li>The service provides at least one dataset in the
+ * {@link FillResponse fill responses} associated with the context.
+ * <li>The last {@link FillResponse fill responses} associated with the context has the
+ * {@link FillResponse#FLAG_TRACK_CONTEXT_COMMITED} flag.
+ * </ul>
+ *
+ * <p>See {@link android.view.autofill.AutofillManager} for more information about autofill
+ * contexts.
+ */
+ public static final int TYPE_CONTEXT_COMMITTED = 4;
+
+ /**
+ * A dataset selector was shown.
+ *
+ * <p>This event is fired whenever the autofill UI was presented to the user.</p>
+ */
+ public static final int TYPE_DATASETS_SHOWN = 5;
+
+ /** @hide */
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_DATASET_SELECTED,
+ TYPE_DATASET_AUTHENTICATION_SELECTED,
+ TYPE_AUTHENTICATION_SELECTED,
+ TYPE_SAVE_SHOWN,
+ TYPE_CONTEXT_COMMITTED,
+ TYPE_DATASETS_SHOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface EventIds{}
+
+ @EventIds private final int mEventType;
+ @Nullable private final String mDatasetId;
+ @Nullable private final Bundle mClientState;
+
+ // Note: mSelectedDatasetIds is stored as List<> instead of Set because Session already
+ // stores it as List
+ @Nullable private final List<String> mSelectedDatasetIds;
+ @Nullable private final ArraySet<String> mIgnoredDatasetIds;
+
+ @Nullable private final ArrayList<AutofillId> mChangedFieldIds;
+ @Nullable private final ArrayList<String> mChangedDatasetIds;
+
+ @Nullable private final ArrayList<AutofillId> mManuallyFilledFieldIds;
+ @Nullable private final ArrayList<ArrayList<String>> mManuallyFilledDatasetIds;
+
+ @Nullable private final AutofillId[] mDetectedFieldIds;
+ @Nullable private final FieldClassification[] mDetectedFieldClassifications;
+
+ /**
+ * Returns the type of the event.
+ *
+ * @return The type of the event
+ */
+ public int getType() {
+ return mEventType;
+ }
+
+ /**
+ * Returns the id of dataset the id was on.
+ *
+ * @return The id of dataset, or {@code null} the event is not associated with a dataset.
+ */
+ @Nullable public String getDatasetId() {
+ return mDatasetId;
+ }
+
+ /**
+ * Returns the client state from the {@link FillResponse} used to generate this event.
+ *
+ * <p><b>Note: </b>the state is associated with the app that was autofilled in the previous
+ * {@link
+ * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)},
+ * which is not necessary the same app being autofilled now.
+ */
+ @Nullable public Bundle getClientState() {
+ return mClientState;
+ }
+
+ /**
+ * Returns which datasets were selected by the user.
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ */
+ @NonNull public Set<String> getSelectedDatasetIds() {
+ return mSelectedDatasetIds == null ? Collections.emptySet()
+ : new ArraySet<>(mSelectedDatasetIds);
+ }
+
+ /**
+ * Returns which datasets were NOT selected by the user.
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ */
+ @NonNull public Set<String> getIgnoredDatasetIds() {
+ return mIgnoredDatasetIds == null ? Collections.emptySet() : mIgnoredDatasetIds;
+ }
+
+ /**
+ * Returns which fields in the selected datasets were changed by the user after the dataset
+ * was selected.
+ *
+ * <p>For example, server provides:
+ *
+ * <pre class="prettyprint">
+ * FillResponse response = new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder(presentation1)
+ * .setId("4815")
+ * .setValue(usernameId, AutofillValue.forText("MrPlow"))
+ * .build())
+ * .addDataset(new Dataset.Builder(presentation2)
+ * .setId("162342")
+ * .setValue(passwordId, AutofillValue.forText("D'OH"))
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>User select both datasets (for username and password) but after the fields are
+ * autofilled, user changes them to:
+ *
+ * <pre class="prettyprint">
+ * username = "ElBarto";
+ * password = "AyCaramba";
+ * </pre>
+ *
+ * <p>Then the result is the following map:
+ *
+ * <pre class="prettyprint">
+ * usernameId => "4815"
+ * passwordId => "162342"
+ * </pre>
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ *
+ * @return map map whose key is the id of the change fields, and value is the id of
+ * dataset that has that field and was selected by the user.
+ */
+ @NonNull public Map<AutofillId, String> getChangedFields() {
+ if (mChangedFieldIds == null || mChangedDatasetIds == null) {
+ return Collections.emptyMap();
+ }
+
+ final int size = mChangedFieldIds.size();
+ final ArrayMap<AutofillId, String> changedFields = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ changedFields.put(mChangedFieldIds.get(i), mChangedDatasetIds.get(i));
+ }
+ return changedFields;
+ }
+
+ /**
+ * Gets the <a href="AutofillService.html#FieldClassification">field classification</a>
+ * results.
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the
+ * service requested {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...)
+ * field classification}.
+ */
+ @NonNull public Map<AutofillId, FieldClassification> getFieldsClassification() {
+ if (mDetectedFieldIds == null) {
+ return Collections.emptyMap();
+ }
+ final int size = mDetectedFieldIds.length;
+ final ArrayMap<AutofillId, FieldClassification> map = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ final AutofillId id = mDetectedFieldIds[i];
+ final FieldClassification fc = mDetectedFieldClassifications[i];
+ if (sVerbose) {
+ Log.v(TAG, "getFieldsClassification[" + i + "]: id=" + id + ", fc=" + fc);
+ }
+ map.put(id, fc);
+ }
+ return map;
+ }
+
+ /**
+ * Returns which fields were available on datasets provided by the service but manually
+ * entered by the user.
+ *
+ * <p>For example, server provides:
+ *
+ * <pre class="prettyprint">
+ * FillResponse response = new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder(presentation1)
+ * .setId("4815")
+ * .setValue(usernameId, AutofillValue.forText("MrPlow"))
+ * .setValue(passwordId, AutofillValue.forText("AyCaramba"))
+ * .build())
+ * .addDataset(new Dataset.Builder(presentation2)
+ * .setId("162342")
+ * .setValue(usernameId, AutofillValue.forText("ElBarto"))
+ * .setValue(passwordId, AutofillValue.forText("D'OH"))
+ * .build())
+ * .addDataset(new Dataset.Builder(presentation3)
+ * .setId("108")
+ * .setValue(usernameId, AutofillValue.forText("MrPlow"))
+ * .setValue(passwordId, AutofillValue.forText("D'OH"))
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>User doesn't select a dataset but manually enters:
+ *
+ * <pre class="prettyprint">
+ * username = "MrPlow";
+ * password = "D'OH";
+ * </pre>
+ *
+ * <p>Then the result is the following map:
+ *
+ * <pre class="prettyprint">
+ * usernameId => { "4815", "108"}
+ * passwordId => { "162342", "108" }
+ * </pre>
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ *
+ * @return map map whose key is the id of the manually-entered field, and value is the
+ * ids of the datasets that have that value but were not selected by the user.
+ */
+ @NonNull public Map<AutofillId, Set<String>> getManuallyEnteredField() {
+ if (mManuallyFilledFieldIds == null || mManuallyFilledDatasetIds == null) {
+ return Collections.emptyMap();
+ }
+
+ final int size = mManuallyFilledFieldIds.size();
+ final Map<AutofillId, Set<String>> manuallyFilledFields = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ final AutofillId fieldId = mManuallyFilledFieldIds.get(i);
+ final ArrayList<String> datasetIds = mManuallyFilledDatasetIds.get(i);
+ manuallyFilledFields.put(fieldId, new ArraySet<>(datasetIds));
+ }
+ return manuallyFilledFields;
+ }
+
+ /**
+ * Creates a new event.
+ *
+ * @param eventType The type of the event
+ * @param datasetId The dataset the event was on, or {@code null} if the event was on the
+ * whole response.
+ * @param clientState The client state associated with the event.
+ * @param selectedDatasetIds The ids of datasets selected by the user.
+ * @param ignoredDatasetIds The ids of datasets NOT select by the user.
+ * @param changedFieldIds The ids of fields changed by the user.
+ * @param changedDatasetIds The ids of the datasets that havd values matching the
+ * respective entry on {@code changedFieldIds}.
+ * @param manuallyFilledFieldIds The ids of fields that were manually entered by the user
+ * and belonged to datasets.
+ * @param manuallyFilledDatasetIds The ids of datasets that had values matching the
+ * respective entry on {@code manuallyFilledFieldIds}.
+ * @param detectedFieldClassifications the field classification matches.
+ *
+ * @throws IllegalArgumentException If the length of {@code changedFieldIds} and
+ * {@code changedDatasetIds} doesn't match.
+ * @throws IllegalArgumentException If the length of {@code manuallyFilledFieldIds} and
+ * {@code manuallyFilledDatasetIds} doesn't match.
+ *
+ * @hide
+ */
+ public Event(int eventType, @Nullable String datasetId, @Nullable Bundle clientState,
+ @Nullable List<String> selectedDatasetIds,
+ @Nullable ArraySet<String> ignoredDatasetIds,
+ @Nullable ArrayList<AutofillId> changedFieldIds,
+ @Nullable ArrayList<String> changedDatasetIds,
+ @Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
+ @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
+ @Nullable AutofillId[] detectedFieldIds,
+ @Nullable FieldClassification[] detectedFieldClassifications) {
+ mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_DATASETS_SHOWN,
+ "eventType");
+ mDatasetId = datasetId;
+ mClientState = clientState;
+ mSelectedDatasetIds = selectedDatasetIds;
+ mIgnoredDatasetIds = ignoredDatasetIds;
+ if (changedFieldIds != null) {
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(changedFieldIds)
+ && changedDatasetIds != null
+ && changedFieldIds.size() == changedDatasetIds.size(),
+ "changed ids must have same length and not be empty");
+ }
+ mChangedFieldIds = changedFieldIds;
+ mChangedDatasetIds = changedDatasetIds;
+ if (manuallyFilledFieldIds != null) {
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(manuallyFilledFieldIds)
+ && manuallyFilledDatasetIds != null
+ && manuallyFilledFieldIds.size() == manuallyFilledDatasetIds.size(),
+ "manually filled ids must have same length and not be empty");
+ }
+ mManuallyFilledFieldIds = manuallyFilledFieldIds;
+ mManuallyFilledDatasetIds = manuallyFilledDatasetIds;
+
+ mDetectedFieldIds = detectedFieldIds;
+ mDetectedFieldClassifications = detectedFieldClassifications;
+ }
+
+ @Override
+ public String toString() {
+ return "FillEvent [datasetId=" + mDatasetId
+ + ", type=" + mEventType
+ + ", selectedDatasets=" + mSelectedDatasetIds
+ + ", ignoredDatasetIds=" + mIgnoredDatasetIds
+ + ", changedFieldIds=" + mChangedFieldIds
+ + ", changedDatasetsIds=" + mChangedDatasetIds
+ + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds
+ + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds
+ + ", detectedFieldIds=" + Arrays.toString(mDetectedFieldIds)
+ + ", detectedFieldClassifications ="
+ + Arrays.toString(mDetectedFieldClassifications)
+ + "]";
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<FillEventHistory> CREATOR =
+ new Parcelable.Creator<FillEventHistory>() {
+ @Override
+ public FillEventHistory createFromParcel(Parcel parcel) {
+ FillEventHistory selection = new FillEventHistory(0, parcel.readBundle());
+
+ final int numEvents = parcel.readInt();
+ for (int i = 0; i < numEvents; i++) {
+ final int eventType = parcel.readInt();
+ final String datasetId = parcel.readString();
+ final Bundle clientState = parcel.readBundle();
+ final ArrayList<String> selectedDatasetIds = parcel.createStringArrayList();
+ @SuppressWarnings("unchecked")
+ final ArraySet<String> ignoredDatasets =
+ (ArraySet<String>) parcel.readArraySet(null);
+ final ArrayList<AutofillId> changedFieldIds =
+ parcel.createTypedArrayList(AutofillId.CREATOR);
+ final ArrayList<String> changedDatasetIds = parcel.createStringArrayList();
+
+ final ArrayList<AutofillId> manuallyFilledFieldIds =
+ parcel.createTypedArrayList(AutofillId.CREATOR);
+ final ArrayList<ArrayList<String>> manuallyFilledDatasetIds;
+ if (manuallyFilledFieldIds != null) {
+ final int size = manuallyFilledFieldIds.size();
+ manuallyFilledDatasetIds = new ArrayList<>(size);
+ for (int j = 0; j < size; j++) {
+ manuallyFilledDatasetIds.add(parcel.createStringArrayList());
+ }
+ } else {
+ manuallyFilledDatasetIds = null;
+ }
+ final AutofillId[] detectedFieldIds = parcel.readParcelableArray(null,
+ AutofillId.class);
+ final FieldClassification[] detectedFieldClassifications =
+ (detectedFieldIds != null)
+ ? FieldClassification.readArrayFromParcel(parcel)
+ : null;
+
+ selection.addEvent(new Event(eventType, datasetId, clientState,
+ selectedDatasetIds, ignoredDatasets,
+ changedFieldIds, changedDatasetIds,
+ manuallyFilledFieldIds, manuallyFilledDatasetIds,
+ detectedFieldIds, detectedFieldClassifications));
+ }
+ return selection;
+ }
+
+ @Override
+ public FillEventHistory[] newArray(int size) {
+ return new FillEventHistory[size];
+ }
+ };
+}
diff --git a/android/service/autofill/FillRequest.java b/android/service/autofill/FillRequest.java
new file mode 100644
index 0000000..d94160c
--- /dev/null
+++ b/android/service/autofill/FillRequest.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+import com.android.internal.util.DataClass;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents a request to an autofill service
+ * to interpret the screen and provide information to the system which views are
+ * interesting for saving and what are the possible ways to fill the inputs on
+ * the screen if applicable.
+ *
+ * @see AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)
+ */
+@DataClass(
+ genToString = true,
+ genHiddenConstructor = true,
+ genHiddenConstDefs = true)
+public final class FillRequest implements Parcelable {
+
+ /**
+ * Indicates autofill was explicitly requested by the user.
+ *
+ * <p>Users typically make an explicit request to autofill a screen in two situations:
+ * <ul>
+ * <li>The app disabled autofill (using {@link View#setImportantForAutofill(int)}.
+ * <li>The service could not figure out how to autofill a screen (but the user knows the
+ * service has data for that app).
+ * </ul>
+ *
+ * <p>This flag is particularly useful for the second case. For example, the service could offer
+ * a complex UI where the user can map which screen views belong to each user data, or it could
+ * offer a simpler UI where the user picks the data for just the view used to trigger the
+ * request (that would be the view whose
+ * {@link android.app.assist.AssistStructure.ViewNode#isFocused()} method returns {@code true}).
+ *
+ * <p>An explicit autofill request is triggered when the
+ * {@link android.view.autofill.AutofillManager#requestAutofill(View)} or
+ * {@link android.view.autofill.AutofillManager#requestAutofill(View, int, android.graphics.Rect)}
+ * is called. For example, standard {@link android.widget.TextView} views show an
+ * {@code AUTOFILL} option in the overflow menu that triggers such request.
+ */
+ public static final @RequestFlags int FLAG_MANUAL_REQUEST = 0x1;
+
+ /**
+ * Indicates this request was made using
+ * <a href="AutofillService.html#CompatibilityMode">compatibility mode</a>.
+ */
+ public static final @RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST = 0x2;
+
+ /**
+ * Indicates the request came from a password field.
+ *
+ * (TODO: b/141703197) Temporary fix for augmented autofill showing passwords.
+ *
+ * @hide
+ */
+ public static final @RequestFlags int FLAG_PASSWORD_INPUT_TYPE = 0x4;
+
+ /** @hide */
+ public static final int INVALID_REQUEST_ID = Integer.MIN_VALUE;
+
+ /**
+ * Gets the unique id of this request.
+ */
+ private final int mId;
+
+ /**
+ * Gets the contexts associated with each previous fill request.
+ *
+ * <p><b>Note:</b> Starting on Android {@link android.os.Build.VERSION_CODES#Q}, it could also
+ * include contexts from requests whose {@link SaveInfo} had the
+ * {@link SaveInfo#FLAG_DELAY_SAVE} flag.
+ */
+ private final @NonNull List<FillContext> mFillContexts;
+
+ /**
+ * Gets the latest client state bundle set by the service in a
+ * {@link FillResponse.Builder#setClientState(Bundle) fill response}.
+ *
+ * <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state
+ * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On
+ * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of
+ * an authenticated request through the
+ * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are
+ * also considered (and take precedence when set).
+ *
+ * @return The client state.
+ */
+ private final @Nullable Bundle mClientState;
+
+ /**
+ * Gets the flags associated with this request.
+ *
+ * @return any combination of {@link #FLAG_MANUAL_REQUEST} and
+ * {@link #FLAG_COMPATIBILITY_MODE_REQUEST}.
+ */
+ private final @RequestFlags int mFlags;
+
+ /**
+ * Gets the {@link InlineSuggestionsRequest} associated
+ * with this request.
+ *
+ * <p>Autofill Framework will send a {@code @non-null} {@link InlineSuggestionsRequest} if
+ * currently inline suggestions are supported and can be displayed. If the Autofill service
+ * wants to show inline suggestions, they may return {@link Dataset} with valid
+ * {@link InlinePresentation}.</p>
+ *
+ * <p>The Autofill Service must set supportsInlineSuggestions in its XML to enable support
+ * for inline suggestions.</p>
+ *
+ * @return the suggestionspec
+ */
+ private final @Nullable InlineSuggestionsRequest mInlineSuggestionsRequest;
+
+ private void onConstructed() {
+ Preconditions.checkCollectionElementsNotNull(mFillContexts, "contexts");
+ }
+
+
+
+ // Code below generated by codegen v1.0.15.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/FillRequest.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /** @hide */
+ @IntDef(flag = true, prefix = "FLAG_", value = {
+ FLAG_MANUAL_REQUEST,
+ FLAG_COMPATIBILITY_MODE_REQUEST,
+ FLAG_PASSWORD_INPUT_TYPE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface RequestFlags {}
+
+ /** @hide */
+ @DataClass.Generated.Member
+ public static String requestFlagsToString(@RequestFlags int value) {
+ return com.android.internal.util.BitUtils.flagsToString(
+ value, FillRequest::singleRequestFlagsToString);
+ }
+
+ @DataClass.Generated.Member
+ static String singleRequestFlagsToString(@RequestFlags int value) {
+ switch (value) {
+ case FLAG_MANUAL_REQUEST:
+ return "FLAG_MANUAL_REQUEST";
+ case FLAG_COMPATIBILITY_MODE_REQUEST:
+ return "FLAG_COMPATIBILITY_MODE_REQUEST";
+ case FLAG_PASSWORD_INPUT_TYPE:
+ return "FLAG_PASSWORD_INPUT_TYPE";
+ default: return Integer.toHexString(value);
+ }
+ }
+
+ /**
+ * Creates a new FillRequest.
+ *
+ * @param id
+ * Gets the unique id of this request.
+ * @param fillContexts
+ * Gets the contexts associated with each previous fill request.
+ *
+ * <p><b>Note:</b> Starting on Android {@link android.os.Build.VERSION_CODES#Q}, it could also
+ * include contexts from requests whose {@link SaveInfo} had the
+ * {@link SaveInfo#FLAG_DELAY_SAVE} flag.
+ * @param clientState
+ * Gets the latest client state bundle set by the service in a
+ * {@link FillResponse.Builder#setClientState(Bundle) fill response}.
+ *
+ * <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state
+ * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On
+ * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of
+ * an authenticated request through the
+ * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are
+ * also considered (and take precedence when set).
+ * @param flags
+ * Gets the flags associated with this request.
+ *
+ * @return any combination of {@link #FLAG_MANUAL_REQUEST} and
+ * {@link #FLAG_COMPATIBILITY_MODE_REQUEST}.
+ * @param inlineSuggestionsRequest
+ * Gets the {@link InlineSuggestionsRequest} associated
+ * with this request.
+ *
+ * <p>Autofill Framework will send a {@code @non-null} {@link InlineSuggestionsRequest} if
+ * currently inline suggestions are supported and can be displayed. If the Autofill service
+ * wants to show inline suggestions, they may return {@link Dataset} with valid
+ * {@link InlinePresentation}.</p>
+ *
+ * <p>The Autofill Service must set supportsInlineSuggestions in its XML to enable support
+ * for inline suggestions.</p>
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public FillRequest(
+ int id,
+ @NonNull List<FillContext> fillContexts,
+ @Nullable Bundle clientState,
+ @RequestFlags int flags,
+ @Nullable InlineSuggestionsRequest inlineSuggestionsRequest) {
+ this.mId = id;
+ this.mFillContexts = fillContexts;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mFillContexts);
+ this.mClientState = clientState;
+ this.mFlags = flags;
+
+ Preconditions.checkFlagsArgument(
+ mFlags,
+ FLAG_MANUAL_REQUEST
+ | FLAG_COMPATIBILITY_MODE_REQUEST
+ | FLAG_PASSWORD_INPUT_TYPE);
+ this.mInlineSuggestionsRequest = inlineSuggestionsRequest;
+
+ onConstructed();
+ }
+
+ /**
+ * Gets the unique id of this request.
+ */
+ @DataClass.Generated.Member
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Gets the contexts associated with each previous fill request.
+ *
+ * <p><b>Note:</b> Starting on Android {@link android.os.Build.VERSION_CODES#Q}, it could also
+ * include contexts from requests whose {@link SaveInfo} had the
+ * {@link SaveInfo#FLAG_DELAY_SAVE} flag.
+ */
+ @DataClass.Generated.Member
+ public @NonNull List<FillContext> getFillContexts() {
+ return mFillContexts;
+ }
+
+ /**
+ * Gets the latest client state bundle set by the service in a
+ * {@link FillResponse.Builder#setClientState(Bundle) fill response}.
+ *
+ * <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state
+ * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On
+ * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of
+ * an authenticated request through the
+ * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are
+ * also considered (and take precedence when set).
+ *
+ * @return The client state.
+ */
+ @DataClass.Generated.Member
+ public @Nullable Bundle getClientState() {
+ return mClientState;
+ }
+
+ /**
+ * Gets the flags associated with this request.
+ *
+ * @return any combination of {@link #FLAG_MANUAL_REQUEST} and
+ * {@link #FLAG_COMPATIBILITY_MODE_REQUEST}.
+ */
+ @DataClass.Generated.Member
+ public @RequestFlags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Gets the {@link InlineSuggestionsRequest} associated
+ * with this request.
+ *
+ * <p>Autofill Framework will send a {@code @non-null} {@link InlineSuggestionsRequest} if
+ * currently inline suggestions are supported and can be displayed. If the Autofill service
+ * wants to show inline suggestions, they may return {@link Dataset} with valid
+ * {@link InlinePresentation}.</p>
+ *
+ * <p>The Autofill Service must set supportsInlineSuggestions in its XML to enable support
+ * for inline suggestions.</p>
+ *
+ * @return the suggestionspec
+ */
+ @DataClass.Generated.Member
+ public @Nullable InlineSuggestionsRequest getInlineSuggestionsRequest() {
+ return mInlineSuggestionsRequest;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "FillRequest { " +
+ "id = " + mId + ", " +
+ "fillContexts = " + mFillContexts + ", " +
+ "clientState = " + mClientState + ", " +
+ "flags = " + requestFlagsToString(mFlags) + ", " +
+ "inlineSuggestionsRequest = " + mInlineSuggestionsRequest +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mClientState != null) flg |= 0x4;
+ if (mInlineSuggestionsRequest != null) flg |= 0x10;
+ dest.writeByte(flg);
+ dest.writeInt(mId);
+ dest.writeParcelableList(mFillContexts, flags);
+ if (mClientState != null) dest.writeBundle(mClientState);
+ dest.writeInt(mFlags);
+ if (mInlineSuggestionsRequest != null) dest.writeTypedObject(mInlineSuggestionsRequest, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ FillRequest(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ int id = in.readInt();
+ List<FillContext> fillContexts = new ArrayList<>();
+ in.readParcelableList(fillContexts, FillContext.class.getClassLoader());
+ Bundle clientState = (flg & 0x4) == 0 ? null : in.readBundle();
+ int flags = in.readInt();
+ InlineSuggestionsRequest inlineSuggestionsRequest = (flg & 0x10) == 0 ? null : (InlineSuggestionsRequest) in.readTypedObject(InlineSuggestionsRequest.CREATOR);
+
+ this.mId = id;
+ this.mFillContexts = fillContexts;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mFillContexts);
+ this.mClientState = clientState;
+ this.mFlags = flags;
+
+ Preconditions.checkFlagsArgument(
+ mFlags,
+ FLAG_MANUAL_REQUEST
+ | FLAG_COMPATIBILITY_MODE_REQUEST
+ | FLAG_PASSWORD_INPUT_TYPE);
+ this.mInlineSuggestionsRequest = inlineSuggestionsRequest;
+
+ onConstructed();
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<FillRequest> CREATOR
+ = new Parcelable.Creator<FillRequest>() {
+ @Override
+ public FillRequest[] newArray(int size) {
+ return new FillRequest[size];
+ }
+
+ @Override
+ public FillRequest createFromParcel(@NonNull Parcel in) {
+ return new FillRequest(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1588119440090L,
+ codegenVersion = "1.0.15",
+ sourceFile = "frameworks/base/core/java/android/service/autofill/FillRequest.java",
+ inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\[email protected](genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/service/autofill/FillResponse.java b/android/service/autofill/FillResponse.java
new file mode 100644
index 0000000..bc08b84
--- /dev/null
+++ b/android/service/autofill/FillResponse.java
@@ -0,0 +1,863 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.service.autofill.AutofillServiceHelper.assertValid;
+import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.app.Activity;
+import android.content.IntentSender;
+import android.content.pm.ParceledListSlice;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.autofill.AutofillId;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Response for an {@link
+ * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}.
+ *
+ * <p>See the main {@link AutofillService} documentation for more details and examples.
+ */
+public final class FillResponse implements Parcelable {
+
+ /**
+ * Flag used to generate {@link FillEventHistory.Event events} of type
+ * {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}—if this flag is not passed to
+ * {@link Builder#setFlags(int)}, these events are not generated.
+ */
+ public static final int FLAG_TRACK_CONTEXT_COMMITED = 0x1;
+
+ /**
+ * Flag used to change the behavior of {@link FillResponse.Builder#disableAutofill(long)}—
+ * when this flag is passed to {@link Builder#setFlags(int)}, autofill is disabled only for the
+ * activiy that generated the {@link FillRequest}, not the whole app.
+ */
+ public static final int FLAG_DISABLE_ACTIVITY_ONLY = 0x2;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_TRACK_CONTEXT_COMMITED,
+ FLAG_DISABLE_ACTIVITY_ONLY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface FillResponseFlags {}
+
+ private final @Nullable ParceledListSlice<Dataset> mDatasets;
+ private final @Nullable SaveInfo mSaveInfo;
+ private final @Nullable Bundle mClientState;
+ private final @Nullable RemoteViews mPresentation;
+ private final @Nullable InlinePresentation mInlinePresentation;
+ private final @Nullable RemoteViews mHeader;
+ private final @Nullable RemoteViews mFooter;
+ private final @Nullable IntentSender mAuthentication;
+ private final @Nullable AutofillId[] mAuthenticationIds;
+ private final @Nullable AutofillId[] mIgnoredIds;
+ private final long mDisableDuration;
+ private final @Nullable AutofillId[] mFieldClassificationIds;
+ private final int mFlags;
+ private int mRequestId;
+ private final @Nullable UserData mUserData;
+ private final @Nullable int[] mCancelIds;
+ private final boolean mSupportsInlineSuggestions;
+
+ private FillResponse(@NonNull Builder builder) {
+ mDatasets = (builder.mDatasets != null) ? new ParceledListSlice<>(builder.mDatasets) : null;
+ mSaveInfo = builder.mSaveInfo;
+ mClientState = builder.mClientState;
+ mPresentation = builder.mPresentation;
+ mInlinePresentation = builder.mInlinePresentation;
+ mHeader = builder.mHeader;
+ mFooter = builder.mFooter;
+ mAuthentication = builder.mAuthentication;
+ mAuthenticationIds = builder.mAuthenticationIds;
+ mIgnoredIds = builder.mIgnoredIds;
+ mDisableDuration = builder.mDisableDuration;
+ mFieldClassificationIds = builder.mFieldClassificationIds;
+ mFlags = builder.mFlags;
+ mRequestId = INVALID_REQUEST_ID;
+ mUserData = builder.mUserData;
+ mCancelIds = builder.mCancelIds;
+ mSupportsInlineSuggestions = builder.mSupportsInlineSuggestions;
+ }
+
+ /** @hide */
+ public @Nullable Bundle getClientState() {
+ return mClientState;
+ }
+
+ /** @hide */
+ public @Nullable List<Dataset> getDatasets() {
+ return (mDatasets != null) ? mDatasets.getList() : null;
+ }
+
+ /** @hide */
+ public @Nullable SaveInfo getSaveInfo() {
+ return mSaveInfo;
+ }
+
+ /** @hide */
+ public @Nullable RemoteViews getPresentation() {
+ return mPresentation;
+ }
+
+ /** @hide */
+ public @Nullable InlinePresentation getInlinePresentation() {
+ return mInlinePresentation;
+ }
+
+ /** @hide */
+ public @Nullable RemoteViews getHeader() {
+ return mHeader;
+ }
+
+ /** @hide */
+ public @Nullable RemoteViews getFooter() {
+ return mFooter;
+ }
+
+ /** @hide */
+ public @Nullable IntentSender getAuthentication() {
+ return mAuthentication;
+ }
+
+ /** @hide */
+ public @Nullable AutofillId[] getAuthenticationIds() {
+ return mAuthenticationIds;
+ }
+
+ /** @hide */
+ public @Nullable AutofillId[] getIgnoredIds() {
+ return mIgnoredIds;
+ }
+
+ /** @hide */
+ public long getDisableDuration() {
+ return mDisableDuration;
+ }
+
+ /** @hide */
+ public @Nullable AutofillId[] getFieldClassificationIds() {
+ return mFieldClassificationIds;
+ }
+
+ /** @hide */
+ public @Nullable UserData getUserData() {
+ return mUserData;
+ }
+
+ /** @hide */
+ @TestApi
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Associates a {@link FillResponse} to a request.
+ *
+ * <p>Set inside of the {@link FillCallback} code, not the {@link AutofillService}.
+ *
+ * @param requestId The id of the request to associate the response to.
+ *
+ * @hide
+ */
+ public void setRequestId(int requestId) {
+ mRequestId = requestId;
+ }
+
+ /** @hide */
+ public int getRequestId() {
+ return mRequestId;
+ }
+
+ /** @hide */
+ @Nullable
+ public int[] getCancelIds() {
+ return mCancelIds;
+ }
+
+ /** @hide */
+ public boolean supportsInlineSuggestions() {
+ return mSupportsInlineSuggestions;
+ }
+
+ /**
+ * Builder for {@link FillResponse} objects. You must to provide at least
+ * one dataset or set an authentication intent with a presentation view.
+ */
+ public static final class Builder {
+ private ArrayList<Dataset> mDatasets;
+ private SaveInfo mSaveInfo;
+ private Bundle mClientState;
+ private RemoteViews mPresentation;
+ private InlinePresentation mInlinePresentation;
+ private RemoteViews mHeader;
+ private RemoteViews mFooter;
+ private IntentSender mAuthentication;
+ private AutofillId[] mAuthenticationIds;
+ private AutofillId[] mIgnoredIds;
+ private long mDisableDuration;
+ private AutofillId[] mFieldClassificationIds;
+ private int mFlags;
+ private boolean mDestroyed;
+ private UserData mUserData;
+ private int[] mCancelIds;
+ private boolean mSupportsInlineSuggestions;
+
+ /**
+ * Triggers a custom UI before before autofilling the screen with any data set in this
+ * response.
+ *
+ * <p><b>Note:</b> Although the name of this method suggests that it should be used just for
+ * authentication flow, it can be used for other advanced flows; see {@link AutofillService}
+ * for examples.
+ *
+ * <p>This is typically useful when a user interaction is required to unlock their
+ * data vault if you encrypt the data set labels and data set data. It is recommended
+ * to encrypt only the sensitive data and not the data set labels which would allow
+ * auth on the data set level leading to a better user experience. Note that if you
+ * use sensitive data as a label, for example an email address, then it should also
+ * be encrypted. The provided {@link android.app.PendingIntent intent} must be an
+ * {@link Activity} which implements your authentication flow. Also if you provide an auth
+ * intent you also need to specify the presentation view to be shown in the fill UI
+ * for the user to trigger your authentication flow.
+ *
+ * <p>When a user triggers autofill, the system launches the provided intent
+ * whose extras will have the
+ * {@link android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen
+ * content} and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE
+ * client state}. Once you complete your authentication flow you should set the
+ * {@link Activity} result to {@link android.app.Activity#RESULT_OK} and set the
+ * {@link android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra
+ * with the fully populated {@link FillResponse response} (or {@code null} if the screen
+ * cannot be autofilled).
+ *
+ * <p>For example, if you provided an empty {@link FillResponse response} because the
+ * user's data was locked and marked that the response needs an authentication then
+ * in the response returned if authentication succeeds you need to provide all
+ * available data sets some of which may need to be further authenticated, for
+ * example a credit card whose CVV needs to be entered.
+ *
+ * <p>If you provide an authentication intent you must also provide a presentation
+ * which is used to visualize visualize the response for triggering the authentication
+ * flow.
+ *
+ * <p><b>Note:</b> Do not make the provided pending intent
+ * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the
+ * platform needs to fill in the authentication arguments.
+ *
+ * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color
+ * or background color: Autofill on different platforms may have different themes.
+ *
+ * @param authentication Intent to an activity with your authentication flow.
+ * @param presentation The presentation to visualize the response.
+ * @param ids id of Views that when focused will display the authentication UI.
+ *
+ * @return This builder.
+ *
+ * @throws IllegalArgumentException if any of the following occurs:
+ * <ul>
+ * <li>{@code ids} is {@code null}</li>
+ * <li>{@code ids} is empty</li>
+ * <li>{@code ids} contains a {@code null} element</li>
+ * <li>both {@code authentication} and {@code presentation} are {@code null}</li>
+ * <li>both {@code authentication} and {@code presentation} are non-{@code null}</li>
+ * </ul>
+ *
+ * @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a
+ * {@link #setFooter(RemoteViews) footer} are already set for this builder.
+ *
+ * @see android.app.PendingIntent#getIntentSender()
+ */
+ @NonNull
+ public Builder setAuthentication(@NonNull AutofillId[] ids,
+ @Nullable IntentSender authentication, @Nullable RemoteViews presentation) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ if (mHeader != null || mFooter != null) {
+ throw new IllegalStateException("Already called #setHeader() or #setFooter()");
+ }
+
+ if (authentication == null ^ presentation == null) {
+ throw new IllegalArgumentException("authentication and presentation"
+ + " must be both non-null or null");
+ }
+ mAuthentication = authentication;
+ mPresentation = presentation;
+ mAuthenticationIds = assertValid(ids);
+ return this;
+ }
+
+ /**
+ * Triggers a custom UI before before autofilling the screen with any data set in this
+ * response.
+ *
+ * <p><b>Note:</b> Although the name of this method suggests that it should be used just for
+ * authentication flow, it can be used for other advanced flows; see {@link AutofillService}
+ * for examples.
+ *
+ * <p>This method is similar to
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, but also accepts
+ * an {@link InlinePresentation} presentation which is required for authenticating through
+ * the inline autofill flow.
+ *
+ * <p><b>Note:</b> {@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} does
+ * not work with {@link InlinePresentation}.</p>
+ *
+ * @param authentication Intent to an activity with your authentication flow.
+ * @param presentation The presentation to visualize the response.
+ * @param inlinePresentation The inlinePresentation to visualize the response inline.
+ * @param ids id of Views that when focused will display the authentication UI.
+ *
+ * @return This builder.
+ *
+ * @throws IllegalArgumentException if any of the following occurs:
+ * <ul>
+ * <li>{@code ids} is {@code null}</li>
+ * <li>{@code ids} is empty</li>
+ * <li>{@code ids} contains a {@code null} element</li>
+ * <li>both {@code authentication} and {@code presentation} are {@code null}</li>
+ * <li>both {@code authentication} and {@code presentation} are non-{@code null}</li>
+ * <li>both {@code authentication} and {@code inlinePresentation} are {@code null}</li>
+ * <li>both {@code authentication} and {@code inlinePresentation} are
+ * non-{@code null}</li>
+ * </ul>
+ *
+ * @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a
+ * {@link #setFooter(RemoteViews) footer} are already set for this builder.
+ *
+ * @see android.app.PendingIntent#getIntentSender()
+ */
+ @NonNull
+ public Builder setAuthentication(@NonNull AutofillId[] ids,
+ @Nullable IntentSender authentication, @Nullable RemoteViews presentation,
+ @Nullable InlinePresentation inlinePresentation) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ if (mHeader != null || mFooter != null) {
+ throw new IllegalStateException("Already called #setHeader() or #setFooter()");
+ }
+
+ if (authentication == null ^ (presentation == null && inlinePresentation == null)) {
+ throw new IllegalArgumentException("authentication and presentation "
+ + "(dropdown or inline), must be both non-null or null");
+ }
+ mAuthentication = authentication;
+ mPresentation = presentation;
+ mInlinePresentation = inlinePresentation;
+ mAuthenticationIds = assertValid(ids);
+ return this;
+ }
+
+ /**
+ * Specifies views that should not trigger new
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
+ * FillCallback)} requests.
+ *
+ * <p>This is typically used when the service cannot autofill the view; for example, a
+ * text field representing the result of a Captcha challenge.
+ */
+ @NonNull
+ public Builder setIgnoredIds(AutofillId...ids) {
+ throwIfDestroyed();
+ mIgnoredIds = ids;
+ return this;
+ }
+
+ /**
+ * Adds a new {@link Dataset} to this response.
+ *
+ * <p><b>Note: </b> on Android {@link android.os.Build.VERSION_CODES#O}, the total number of
+ * datasets is limited by the Binder transaction size, so it's recommended to keep it
+ * small (in the range of 10-20 at most) and use pagination by adding a fake
+ * {@link Dataset.Builder#setAuthentication(IntentSender) authenticated dataset} at the end
+ * with a presentation string like "Next 10" that would return a new {@link FillResponse}
+ * with the next 10 datasets, and so on. This limitation was lifted on
+ * Android {@link android.os.Build.VERSION_CODES#O_MR1}, although the Binder transaction
+ * size can still be reached if each dataset itself is too big.
+ *
+ * @return This builder.
+ */
+ @NonNull
+ public Builder addDataset(@Nullable Dataset dataset) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ if (dataset == null) {
+ return this;
+ }
+ if (mDatasets == null) {
+ mDatasets = new ArrayList<>();
+ }
+ if (!mDatasets.add(dataset)) {
+ return this;
+ }
+ return this;
+ }
+
+ /**
+ * Sets the {@link SaveInfo} associated with this response.
+ *
+ * @return This builder.
+ */
+ public @NonNull Builder setSaveInfo(@NonNull SaveInfo saveInfo) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ mSaveInfo = saveInfo;
+ return this;
+ }
+
+ /**
+ * Sets a bundle with state that is passed to subsequent APIs that manipulate this response.
+ *
+ * <p>You can use this bundle to store intermediate state that is passed to subsequent calls
+ * to {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
+ * FillCallback)} and {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}, and
+ * you can also retrieve it by calling {@link FillEventHistory.Event#getClientState()}.
+ *
+ * <p>If this method is called on multiple {@link FillResponse} objects for the same
+ * screen, just the latest bundle is passed back to the service.
+ *
+ * @param clientState The custom client state.
+ * @return This builder.
+ */
+ @NonNull
+ public Builder setClientState(@Nullable Bundle clientState) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ mClientState = clientState;
+ return this;
+ }
+
+ /**
+ * Sets which fields are used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>
+ *
+ * <p><b>Note:</b> This method automatically adds the
+ * {@link FillResponse#FLAG_TRACK_CONTEXT_COMMITED} to the {@link #setFlags(int) flags}.
+
+ * @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}.
+ */
+ @NonNull
+ public Builder setFieldClassificationIds(@NonNull AutofillId... ids) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ Preconditions.checkArrayElementsNotNull(ids, "ids");
+ Preconditions.checkArgumentInRange(ids.length, 1,
+ UserData.getMaxFieldClassificationIdsSize(), "ids length");
+ mFieldClassificationIds = ids;
+ mFlags |= FLAG_TRACK_CONTEXT_COMMITED;
+ return this;
+ }
+
+ /**
+ * Sets flags changing the response behavior.
+ *
+ * @param flags a combination of {@link #FLAG_TRACK_CONTEXT_COMMITED} and
+ * {@link #FLAG_DISABLE_ACTIVITY_ONLY}, or {@code 0}.
+ *
+ * @return This builder.
+ */
+ @NonNull
+ public Builder setFlags(@FillResponseFlags int flags) {
+ throwIfDestroyed();
+ mFlags = Preconditions.checkFlagsArgument(flags,
+ FLAG_TRACK_CONTEXT_COMMITED | FLAG_DISABLE_ACTIVITY_ONLY);
+ return this;
+ }
+
+ /**
+ * Disables autofill for the app or activity.
+ *
+ * <p>This method is useful to optimize performance in cases where the service knows it
+ * can not autofill an app—for example, when the service has a list of "blacklisted"
+ * apps such as office suites.
+ *
+ * <p>By default, it disables autofill for all activities in the app, unless the response is
+ * {@link #setFlags(int) flagged} with {@link #FLAG_DISABLE_ACTIVITY_ONLY}.
+ *
+ * <p>Autofill for the app or activity is automatically re-enabled after any of the
+ * following conditions:
+ *
+ * <ol>
+ * <li>{@code duration} milliseconds have passed.
+ * <li>The autofill service for the user has changed.
+ * <li>The device has rebooted.
+ * </ol>
+ *
+ * <p><b>Note:</b> Activities that are running when autofill is re-enabled remain
+ * disabled for autofill until they finish and restart.
+ *
+ * @param duration duration to disable autofill, in milliseconds.
+ *
+ * @return this builder
+ *
+ * @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)}, {@link #setClientState(Bundle)}, or
+ * {@link #setFieldClassificationIds(AutofillId...)} was already called.
+ */
+ @NonNull
+ 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
+ || mFieldClassificationIds != null || mClientState != null) {
+ throw new IllegalStateException("disableAutofill() must be the only method called");
+ }
+
+ mDisableDuration = duration;
+ return this;
+ }
+
+ /**
+ * Sets a header to be shown as the first element in the list of datasets.
+ *
+ * <p>When this method is called, you must also {@link #addDataset(Dataset) add a dataset},
+ * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this
+ * method should only be used on {@link FillResponse FillResponses} that do not require
+ * authentication (as the header could have been set directly in the main presentation in
+ * these cases).
+ *
+ * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color
+ * or background color: Autofill on different platforms may have different themes.
+ *
+ * @param header a presentation to represent the header. This presentation is not clickable
+ * —calling
+ * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would
+ * have no effect.
+ *
+ * @return this builder
+ *
+ * @throws IllegalStateException if an
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) authentication} was
+ * already set for this builder.
+ */
+ // TODO(b/69796626): make it sticky / update javadoc
+ @NonNull
+ public Builder setHeader(@NonNull RemoteViews header) {
+ throwIfDestroyed();
+ throwIfAuthenticationCalled();
+ mHeader = Preconditions.checkNotNull(header);
+ return this;
+ }
+
+ /**
+ * Sets a footer to be shown as the last element in the list of datasets.
+ *
+ * <p>When this method is called, you must also {@link #addDataset(Dataset) add a dataset},
+ * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this
+ * method should only be used on {@link FillResponse FillResponses} that do not require
+ * authentication (as the footer could have been set directly in the main presentation in
+ * these cases).
+ *
+ * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color
+ * or background color: Autofill on different platforms may have different themes.
+ *
+ * @param footer a presentation to represent the footer. This presentation is not clickable
+ * —calling
+ * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would
+ * have no effect.
+ *
+ * @return this builder
+ *
+ * @throws IllegalStateException if the FillResponse
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)
+ * requires authentication}.
+ */
+ // TODO(b/69796626): make it sticky / update javadoc
+ @NonNull
+ public Builder setFooter(@NonNull RemoteViews footer) {
+ throwIfDestroyed();
+ throwIfAuthenticationCalled();
+ mFooter = Preconditions.checkNotNull(footer);
+ return this;
+ }
+
+ /**
+ * Sets a specific {@link UserData} for field classification for this request only.
+ *
+ * <p>Any fields in this UserData will override corresponding fields in the generic
+ * UserData object
+ *
+ * @return this builder
+ * @throws IllegalStateException if the FillResponse
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)
+ * requires authentication}.
+ */
+ @NonNull
+ public Builder setUserData(@NonNull UserData userData) {
+ throwIfDestroyed();
+ throwIfAuthenticationCalled();
+ mUserData = Preconditions.checkNotNull(userData);
+ return this;
+ }
+
+ /**
+ * Sets target resource IDs of the child view in {@link RemoteViews Presentation Template}
+ * which will cancel the session when clicked.
+ * Those targets will be respectively applied to a child of the header, footer and
+ * each {@link Dataset}.
+ *
+ * @param ids array of the resource id. Empty list or non-existing id has no effect.
+ *
+ * @return this builder
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ */
+ @NonNull
+ public Builder setPresentationCancelIds(@Nullable int[] ids) {
+ throwIfDestroyed();
+ mCancelIds = ids;
+ return this;
+ }
+
+ /**
+ * Builds a new {@link FillResponse} instance.
+ *
+ * @throws IllegalStateException if any of the following conditions occur:
+ * <ol>
+ * <li>{@link #build()} was already called.
+ * <li>No call was made to {@link #addDataset(Dataset)},
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)},
+ * {@link #setSaveInfo(SaveInfo)}, {@link #disableAutofill(long)},
+ * {@link #setClientState(Bundle)},
+ * or {@link #setFieldClassificationIds(AutofillId...)}.
+ * <li>{@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} is called
+ * without any previous calls to {@link #addDataset(Dataset)}.
+ * </ol>
+ *
+ * @return A built response.
+ */
+ @NonNull
+ public FillResponse build() {
+ throwIfDestroyed();
+ if (mAuthentication == null && mDatasets == null && mSaveInfo == 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");
+ }
+ if (mDatasets == null && (mHeader != null || mFooter != null)) {
+ throw new IllegalStateException(
+ "must add at least 1 dataset when using header or footer");
+ }
+
+ if (mDatasets != null) {
+ for (final Dataset dataset : mDatasets) {
+ if (dataset.getFieldInlinePresentation(0) != null) {
+ mSupportsInlineSuggestions = true;
+ break;
+ }
+ }
+ } else if (mInlinePresentation != null) {
+ mSupportsInlineSuggestions = true;
+ }
+
+ mDestroyed = true;
+ return new FillResponse(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+
+ private void throwIfDisableAutofillCalled() {
+ if (mDisableDuration > 0) {
+ throw new IllegalStateException("Already called #disableAutofill()");
+ }
+ }
+
+ private void throwIfAuthenticationCalled() {
+ if (mAuthentication != null) {
+ throw new IllegalStateException("Already called #setAuthentication()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ // TODO: create a dump() method instead
+ final StringBuilder builder = new StringBuilder(
+ "FillResponse : [mRequestId=" + mRequestId);
+ if (mDatasets != null) {
+ builder.append(", datasets=").append(mDatasets.getList());
+ }
+ if (mSaveInfo != null) {
+ builder.append(", saveInfo=").append(mSaveInfo);
+ }
+ if (mClientState != null) {
+ builder.append(", hasClientState");
+ }
+ if (mPresentation != null) {
+ builder.append(", hasPresentation");
+ }
+ if (mInlinePresentation != null) {
+ builder.append(", hasInlinePresentation");
+ }
+ if (mHeader != null) {
+ builder.append(", hasHeader");
+ }
+ if (mFooter != null) {
+ builder.append(", hasFooter");
+ }
+ if (mAuthentication != null) {
+ builder.append(", hasAuthentication");
+ }
+ if (mAuthenticationIds != null) {
+ builder.append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds));
+ }
+ builder.append(", disableDuration=").append(mDisableDuration);
+ if (mFlags != 0) {
+ builder.append(", flags=").append(mFlags);
+ }
+ if (mFieldClassificationIds != null) {
+ builder.append(Arrays.toString(mFieldClassificationIds));
+ }
+ if (mUserData != null) {
+ builder.append(", userData=").append(mUserData);
+ }
+ if (mCancelIds != null) {
+ builder.append(", mCancelIds=").append(mCancelIds.length);
+ }
+ builder.append(", mSupportInlinePresentations=").append(mSupportsInlineSuggestions);
+ return builder.append("]").toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mDatasets, flags);
+ parcel.writeParcelable(mSaveInfo, flags);
+ parcel.writeParcelable(mClientState, flags);
+ parcel.writeParcelableArray(mAuthenticationIds, flags);
+ parcel.writeParcelable(mAuthentication, flags);
+ parcel.writeParcelable(mPresentation, flags);
+ parcel.writeParcelable(mInlinePresentation, flags);
+ parcel.writeParcelable(mHeader, flags);
+ parcel.writeParcelable(mFooter, flags);
+ parcel.writeParcelable(mUserData, flags);
+ parcel.writeParcelableArray(mIgnoredIds, flags);
+ parcel.writeLong(mDisableDuration);
+ parcel.writeParcelableArray(mFieldClassificationIds, flags);
+ parcel.writeInt(mFlags);
+ parcel.writeIntArray(mCancelIds);
+ parcel.writeInt(mRequestId);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<FillResponse> CREATOR =
+ new Parcelable.Creator<FillResponse>() {
+ @Override
+ public FillResponse createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final Builder builder = new Builder();
+ final ParceledListSlice<Dataset> datasetSlice = parcel.readParcelable(null);
+ final List<Dataset> datasets = (datasetSlice != null) ? datasetSlice.getList() : null;
+ final int datasetCount = (datasets != null) ? datasets.size() : 0;
+ for (int i = 0; i < datasetCount; i++) {
+ builder.addDataset(datasets.get(i));
+ }
+ builder.setSaveInfo(parcel.readParcelable(null));
+ builder.setClientState(parcel.readParcelable(null));
+
+ // Sets authentication state.
+ final AutofillId[] authenticationIds = parcel.readParcelableArray(null,
+ AutofillId.class);
+ final IntentSender authentication = parcel.readParcelable(null);
+ final RemoteViews presentation = parcel.readParcelable(null);
+ final InlinePresentation inlinePresentation = parcel.readParcelable(null);
+ if (authenticationIds != null) {
+ builder.setAuthentication(authenticationIds, authentication, presentation,
+ inlinePresentation);
+ }
+ final RemoteViews header = parcel.readParcelable(null);
+ if (header != null) {
+ builder.setHeader(header);
+ }
+ final RemoteViews footer = parcel.readParcelable(null);
+ if (footer != null) {
+ builder.setFooter(footer);
+ }
+ final UserData userData = parcel.readParcelable(null);
+ if (userData != null) {
+ builder.setUserData(userData);
+ }
+
+ builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class));
+ final long disableDuration = parcel.readLong();
+ if (disableDuration > 0) {
+ builder.disableAutofill(disableDuration);
+ }
+ final AutofillId[] fieldClassifactionIds =
+ parcel.readParcelableArray(null, AutofillId.class);
+ if (fieldClassifactionIds != null) {
+ builder.setFieldClassificationIds(fieldClassifactionIds);
+ }
+ builder.setFlags(parcel.readInt());
+ final int[] cancelIds = parcel.createIntArray();
+ builder.setPresentationCancelIds(cancelIds);
+
+ final FillResponse response = builder.build();
+ response.setRequestId(parcel.readInt());
+
+ return response;
+ }
+
+ @Override
+ public FillResponse[] newArray(int size) {
+ return new FillResponse[size];
+ }
+ };
+}
diff --git a/android/service/autofill/ImageTransformation.java b/android/service/autofill/ImageTransformation.java
new file mode 100644
index 0000000..974f0ea
--- /dev/null
+++ b/android/service/autofill/ImageTransformation.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * Replaces the content of a child {@link ImageView} of a
+ * {@link RemoteViews presentation template} with the first image that matches a regular expression
+ * (regex).
+ *
+ * <p>Typically used to display credit card logos. Example:
+ *
+ * <pre class="prettyprint">
+ * new ImageTransformation.Builder(ccNumberId, Pattern.compile("^4815.*$"),
+ * R.drawable.ic_credit_card_logo1, "Brand 1")
+ * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2, "Brand 2")
+ * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3, "Brand 3")
+ * .build();
+ * </pre>
+ *
+ * <p>There is no imposed limit in the number of options, but keep in mind that regexs are
+ * expensive to evaluate, so use the minimum number of regexs and add the most common first
+ * (for example, if this is a tranformation for a credit card logo and the most common credit card
+ * issuers are banks X and Y, add the regexes that resolves these 2 banks first).
+ */
+public final class ImageTransformation extends InternalTransformation implements Transformation,
+ Parcelable {
+ private static final String TAG = "ImageTransformation";
+
+ private final AutofillId mId;
+ private final ArrayList<Option> mOptions;
+
+ private ImageTransformation(Builder builder) {
+ mId = builder.mId;
+ mOptions = builder.mOptions;
+ }
+
+ /** @hide */
+ @TestApi
+ @Override
+ public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
+ int childViewId) throws Exception {
+ final String value = finder.findByAutofillId(mId);
+ if (value == null) {
+ Log.w(TAG, "No view for id " + mId);
+ return;
+ }
+ final int size = mOptions.size();
+ if (sDebug) {
+ Log.d(TAG, size + " multiple options on id " + childViewId + " to compare against");
+ }
+
+ for (int i = 0; i < size; i++) {
+ final Option option = mOptions.get(i);
+ try {
+ if (option.pattern.matcher(value).matches()) {
+ Log.d(TAG, "Found match at " + i + ": " + option);
+ parentTemplate.setImageViewResource(childViewId, option.resId);
+ if (option.contentDescription != null) {
+ parentTemplate.setContentDescription(childViewId,
+ option.contentDescription);
+ }
+ return;
+ }
+ } catch (Exception e) {
+ // Do not log full exception to avoid PII leaking
+ Log.w(TAG, "Error matching regex #" + i + "(" + option.pattern + ") on id "
+ + option.resId + ": " + e.getClass());
+ throw e;
+
+ }
+ }
+ if (sDebug) Log.d(TAG, "No match for " + value);
+ }
+
+ /**
+ * Builder for {@link ImageTransformation} objects.
+ */
+ public static class Builder {
+ private final AutofillId mId;
+ private final ArrayList<Option> mOptions = new ArrayList<>();
+ private boolean mDestroyed;
+
+ /**
+ * Creates a new builder for a autofill id and add a first option.
+ *
+ * @param id id of the screen field that will be used to evaluate whether the image should
+ * be used.
+ * @param regex regular expression defining what should be matched to use this image.
+ * @param resId resource id of the image (in the autofill service's package). The
+ * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
+ *
+ * @deprecated use
+ * {@link #Builder(AutofillId, Pattern, int, CharSequence)} instead.
+ */
+ @Deprecated
+ public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId) {
+ mId = Preconditions.checkNotNull(id);
+ addOption(regex, resId);
+ }
+
+ /**
+ * Creates a new builder for a autofill id and add a first option.
+ *
+ * @param id id of the screen field that will be used to evaluate whether the image should
+ * be used.
+ * @param regex regular expression defining what should be matched to use this image.
+ * @param resId resource id of the image (in the autofill service's package). The
+ * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
+ * @param contentDescription content description to be applied in the child view.
+ */
+ public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId,
+ @NonNull CharSequence contentDescription) {
+ mId = Preconditions.checkNotNull(id);
+ addOption(regex, resId, contentDescription);
+ }
+
+ /**
+ * Adds an option to replace the child view with a different image when the regex matches.
+ *
+ * @param regex regular expression defining what should be matched to use this image.
+ * @param resId resource id of the image (in the autofill service's package). The
+ * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
+ *
+ * @return this build
+ *
+ * @deprecated use {@link #addOption(Pattern, int, CharSequence)} instead.
+ */
+ @Deprecated
+ public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId) {
+ addOptionInternal(regex, resId, null);
+ return this;
+ }
+
+ /**
+ * Adds an option to replace the child view with a different image and content description
+ * when the regex matches.
+ *
+ * @param regex regular expression defining what should be matched to use this image.
+ * @param resId resource id of the image (in the autofill service's package). The
+ * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
+ * @param contentDescription content description to be applied in the child view.
+ *
+ * @return this build
+ */
+ public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId,
+ @NonNull CharSequence contentDescription) {
+ addOptionInternal(regex, resId, Preconditions.checkNotNull(contentDescription));
+ return this;
+ }
+
+ private void addOptionInternal(@NonNull Pattern regex, @DrawableRes int resId,
+ @Nullable CharSequence contentDescription) {
+ throwIfDestroyed();
+
+ Preconditions.checkNotNull(regex);
+ Preconditions.checkArgument(resId != 0);
+
+ mOptions.add(new Option(regex, resId, contentDescription));
+ }
+
+
+ /**
+ * Creates a new {@link ImageTransformation} instance.
+ */
+ public ImageTransformation build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new ImageTransformation(this);
+ }
+
+ private void throwIfDestroyed() {
+ Preconditions.checkState(!mDestroyed, "Already called build()");
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "ImageTransformation: [id=" + mId + ", options=" + mOptions + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mId, flags);
+
+ final int size = mOptions.size();
+ final Pattern[] patterns = new Pattern[size];
+ final int[] resIds = new int[size];
+ final CharSequence[] contentDescriptions = new String[size];
+ for (int i = 0; i < size; i++) {
+ final Option option = mOptions.get(i);
+ patterns[i] = option.pattern;
+ resIds[i] = option.resId;
+ contentDescriptions[i] = option.contentDescription;
+ }
+ parcel.writeSerializable(patterns);
+ parcel.writeIntArray(resIds);
+ parcel.writeCharSequenceArray(contentDescriptions);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<ImageTransformation> CREATOR =
+ new Parcelable.Creator<ImageTransformation>() {
+ @Override
+ public ImageTransformation createFromParcel(Parcel parcel) {
+ final AutofillId id = parcel.readParcelable(null);
+
+ final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
+ final int[] resIds = parcel.createIntArray();
+ final CharSequence[] contentDescriptions = parcel.readCharSequenceArray();
+
+ // 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 CharSequence contentDescription = contentDescriptions[0];
+ final ImageTransformation.Builder builder = (contentDescription != null)
+ ? new ImageTransformation.Builder(id, regexs[0], resIds[0], contentDescription)
+ : new ImageTransformation.Builder(id, regexs[0], resIds[0]);
+
+ final int size = regexs.length;
+ for (int i = 1; i < size; i++) {
+ if (contentDescriptions[i] != null) {
+ builder.addOption(regexs[i], resIds[i], contentDescriptions[i]);
+ } else {
+ builder.addOption(regexs[i], resIds[i]);
+ }
+ }
+
+ return builder.build();
+ }
+
+ @Override
+ public ImageTransformation[] newArray(int size) {
+ return new ImageTransformation[size];
+ }
+ };
+
+ private static final class Option {
+ public final Pattern pattern;
+ public final int resId;
+ public final CharSequence contentDescription;
+
+ Option(Pattern pattern, int resId, CharSequence contentDescription) {
+ this.pattern = pattern;
+ this.resId = resId;
+ this.contentDescription = TextUtils.trimNoCopySpans(contentDescription);
+ }
+ }
+}
diff --git a/android/service/autofill/InlinePresentation.java b/android/service/autofill/InlinePresentation.java
new file mode 100644
index 0000000..9cf1b87
--- /dev/null
+++ b/android/service/autofill/InlinePresentation.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2019 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.NonNull;
+import android.annotation.Size;
+import android.app.slice.Slice;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.widget.inline.InlinePresentationSpec;
+
+import com.android.internal.util.DataClass;
+
+import java.util.List;
+
+/**
+ * Wrapper class holding a {@link Slice} and an {@link InlinePresentationSpec} for rendering UI
+ * for an Inline Suggestion.
+ */
+@DataClass(
+ genToString = true,
+ genHiddenConstDefs = true,
+ genEqualsHashCode = true)
+public final class InlinePresentation implements Parcelable {
+
+
+ /**
+ * Represents the UI content and the action for the inline suggestion.
+ */
+ private final @NonNull Slice mSlice;
+
+ /**
+ * Specifies the UI specification for the inline suggestion.
+ */
+ private final @NonNull InlinePresentationSpec mInlinePresentationSpec;
+
+ /**
+ * Indicates whether the UI should be pinned, hence non-scrollable and non-filterable, in the
+ * host.
+ */
+ private final boolean mPinned;
+
+ /**
+ * Returns the autofill hints set in the slice.
+ *
+ * @hide
+ */
+ @NonNull
+ @Size(min = 0)
+ public String[] getAutofillHints() {
+ List<String> hints = mSlice.getHints();
+ return hints.toArray(new String[hints.size()]);
+ }
+
+
+
+ // Code below generated by codegen v1.0.15.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/InlinePresentation.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new InlinePresentation.
+ *
+ * @param slice
+ * Represents the UI content and the action for the inline suggestion.
+ * @param inlinePresentationSpec
+ * Specifies the UI specification for the inline suggestion.
+ * @param pinned
+ * Indicates whether the UI should be pinned, hence non-scrollable and non-filterable, in the
+ * host.
+ */
+ @DataClass.Generated.Member
+ public InlinePresentation(
+ @NonNull Slice slice,
+ @NonNull InlinePresentationSpec inlinePresentationSpec,
+ boolean pinned) {
+ this.mSlice = slice;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSlice);
+ this.mInlinePresentationSpec = inlinePresentationSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInlinePresentationSpec);
+ this.mPinned = pinned;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * Represents the UI content and the action for the inline suggestion.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Slice getSlice() {
+ return mSlice;
+ }
+
+ /**
+ * Specifies the UI specification for the inline suggestion.
+ */
+ @DataClass.Generated.Member
+ public @NonNull InlinePresentationSpec getInlinePresentationSpec() {
+ return mInlinePresentationSpec;
+ }
+
+ /**
+ * Indicates whether the UI should be pinned, hence non-scrollable and non-filterable, in the
+ * host.
+ */
+ @DataClass.Generated.Member
+ public boolean isPinned() {
+ return mPinned;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "InlinePresentation { " +
+ "slice = " + mSlice + ", " +
+ "inlinePresentationSpec = " + mInlinePresentationSpec + ", " +
+ "pinned = " + mPinned +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@android.annotation.Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(InlinePresentation other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ InlinePresentation that = (InlinePresentation) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && java.util.Objects.equals(mSlice, that.mSlice)
+ && java.util.Objects.equals(mInlinePresentationSpec, that.mInlinePresentationSpec)
+ && mPinned == that.mPinned;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + java.util.Objects.hashCode(mSlice);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mInlinePresentationSpec);
+ _hash = 31 * _hash + Boolean.hashCode(mPinned);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mPinned) flg |= 0x4;
+ dest.writeByte(flg);
+ dest.writeTypedObject(mSlice, flags);
+ dest.writeTypedObject(mInlinePresentationSpec, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ InlinePresentation(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ boolean pinned = (flg & 0x4) != 0;
+ Slice slice = (Slice) in.readTypedObject(Slice.CREATOR);
+ InlinePresentationSpec inlinePresentationSpec = (InlinePresentationSpec) in.readTypedObject(InlinePresentationSpec.CREATOR);
+
+ this.mSlice = slice;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSlice);
+ this.mInlinePresentationSpec = inlinePresentationSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInlinePresentationSpec);
+ this.mPinned = pinned;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<InlinePresentation> CREATOR
+ = new Parcelable.Creator<InlinePresentation>() {
+ @Override
+ public InlinePresentation[] newArray(int size) {
+ return new InlinePresentation[size];
+ }
+
+ @Override
+ public InlinePresentation createFromParcel(@NonNull Parcel in) {
+ return new InlinePresentation(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1586992400667L,
+ codegenVersion = "1.0.15",
+ sourceFile = "frameworks/base/core/java/android/service/autofill/InlinePresentation.java",
+ inputSignatures = "private final @android.annotation.NonNull android.app.slice.Slice mSlice\nprivate final @android.annotation.NonNull android.widget.inline.InlinePresentationSpec mInlinePresentationSpec\nprivate final boolean mPinned\npublic @android.annotation.NonNull @android.annotation.Size(min=0L) java.lang.String[] getAutofillHints()\nclass InlinePresentation extends java.lang.Object implements [android.os.Parcelable]\[email protected](genToString=true, genHiddenConstDefs=true, genEqualsHashCode=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/service/autofill/InlineSuggestionRenderService.java b/android/service/autofill/InlineSuggestionRenderService.java
new file mode 100644
index 0000000..6c22b19
--- /dev/null
+++ b/android/service/autofill/InlineSuggestionRenderService.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2020 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 com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.graphics.PixelFormat;
+import android.os.BaseBundle;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Size;
+import android.view.Display;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+/**
+ * A service that renders an inline presentation view given the {@link InlinePresentation}.
+ *
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public abstract class InlineSuggestionRenderService extends Service {
+
+ private static final String TAG = "InlineSuggestionRenderService";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ *
+ * <p>To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_INLINE_SUGGESTION_RENDER_SERVICE} permission so that
+ * other applications can not abuse it.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.autofill.InlineSuggestionRenderService";
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
+
+ private IInlineSuggestionUiCallback mCallback;
+
+ /**
+ * If the specified {@code width}/{@code height} is an exact value, then it will be returned as
+ * is, otherwise the method tries to measure a size that is just large enough to fit the view
+ * content, within constraints posed by {@code minSize} and {@code maxSize}.
+ *
+ * @param view the view for which we measure the size
+ * @param width the expected width of the view, either an exact value or {@link
+ * ViewGroup.LayoutParams#WRAP_CONTENT}
+ * @param height the expected width of the view, either an exact value or {@link
+ * ViewGroup.LayoutParams#WRAP_CONTENT}
+ * @param minSize the lower bound of the size to be returned
+ * @param maxSize the upper bound of the size to be returned
+ * @return the measured size of the view based on the given size constraints.
+ */
+ private Size measuredSize(@NonNull View view, int width, int height, @NonNull Size minSize,
+ @NonNull Size maxSize) {
+ if (width != ViewGroup.LayoutParams.WRAP_CONTENT
+ && height != ViewGroup.LayoutParams.WRAP_CONTENT) {
+ return new Size(width, height);
+ }
+ int widthMeasureSpec;
+ if (width == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxSize.getWidth(),
+ View.MeasureSpec.AT_MOST);
+ } else {
+ widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
+ }
+ int heightMeasureSpec;
+ if (height == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxSize.getHeight(),
+ View.MeasureSpec.AT_MOST);
+ } else {
+ heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
+ }
+ view.measure(widthMeasureSpec, heightMeasureSpec);
+ return new Size(Math.max(view.getMeasuredWidth(), minSize.getWidth()),
+ Math.max(view.getMeasuredHeight(), minSize.getHeight()));
+ }
+
+ private void handleRenderSuggestion(IInlineSuggestionUiCallback callback,
+ InlinePresentation presentation, int width, int height, IBinder hostInputToken,
+ int displayId) {
+ if (hostInputToken == null) {
+ try {
+ callback.onError();
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling onError()");
+ }
+ return;
+ }
+
+ // When we create the UI it should be for the IME display
+ updateDisplay(displayId);
+ try {
+ final View suggestionView = onRenderSuggestion(presentation, width, height);
+ if (suggestionView == null) {
+ Log.w(TAG, "ExtServices failed to render the inline suggestion view.");
+ try {
+ callback.onError();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Null suggestion view returned by renderer");
+ }
+ return;
+ }
+ mCallback = callback;
+ final Size measuredSize = measuredSize(suggestionView, width, height,
+ presentation.getInlinePresentationSpec().getMinSize(),
+ presentation.getInlinePresentationSpec().getMaxSize());
+ Log.v(TAG, "width=" + width + ", height=" + height + ", measuredSize=" + measuredSize);
+
+ final InlineSuggestionRoot suggestionRoot = new InlineSuggestionRoot(this, callback);
+ suggestionRoot.addView(suggestionView);
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams(measuredSize.getWidth(),
+ measuredSize.getHeight(), WindowManager.LayoutParams.TYPE_APPLICATION, 0,
+ PixelFormat.TRANSPARENT);
+
+ final SurfaceControlViewHost host = new SurfaceControlViewHost(this, getDisplay(),
+ hostInputToken);
+ host.setView(suggestionRoot, lp);
+
+ // Set the suggestion view to be non-focusable so that if its background is set to a
+ // ripple drawable, the ripple won't be shown initially.
+ suggestionView.setFocusable(false);
+ suggestionView.setOnClickListener((v) -> {
+ try {
+ callback.onClick();
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling onClick()");
+ }
+ });
+ final View.OnLongClickListener onLongClickListener =
+ suggestionView.getOnLongClickListener();
+ suggestionView.setOnLongClickListener((v) -> {
+ if (onLongClickListener != null) {
+ onLongClickListener.onLongClick(v);
+ }
+ try {
+ callback.onLongClick();
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling onLongClick()");
+ }
+ return true;
+ });
+
+ sendResult(callback, host.getSurfacePackage(), measuredSize.getWidth(),
+ measuredSize.getHeight());
+ } finally {
+ updateDisplay(Display.DEFAULT_DISPLAY);
+ }
+ }
+
+ private void handleGetInlineSuggestionsRendererInfo(@NonNull RemoteCallback callback) {
+ final Bundle rendererInfo = onGetInlineSuggestionsRendererInfo();
+ callback.sendResult(rendererInfo);
+ }
+
+ private void sendResult(@NonNull IInlineSuggestionUiCallback callback,
+ @Nullable SurfaceControlViewHost.SurfacePackage surface, int width, int height) {
+ try {
+ callback.onContent(surface, width, height);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling onContent(" + surface + ")");
+ }
+ }
+
+ @Override
+ @Nullable
+ public final IBinder onBind(@NonNull Intent intent) {
+ BaseBundle.setShouldDefuse(true);
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return new IInlineSuggestionRenderService.Stub() {
+ @Override
+ public void renderSuggestion(@NonNull IInlineSuggestionUiCallback callback,
+ @NonNull InlinePresentation presentation, int width, int height,
+ @Nullable IBinder hostInputToken, int displayId) {
+ mHandler.sendMessage(
+ obtainMessage(InlineSuggestionRenderService::handleRenderSuggestion,
+ InlineSuggestionRenderService.this, callback, presentation,
+ width, height, hostInputToken, displayId));
+ }
+
+ @Override
+ public void getInlineSuggestionsRendererInfo(@NonNull RemoteCallback callback) {
+ mHandler.sendMessage(obtainMessage(
+ InlineSuggestionRenderService::handleGetInlineSuggestionsRendererInfo,
+ InlineSuggestionRenderService.this, callback));
+ }
+ }.asBinder();
+ }
+
+ Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+ return null;
+ }
+
+ /**
+ * Starts the {@link IntentSender} from the client app.
+ *
+ * @param intentSender the {@link IntentSender} to start the attribution UI from the client
+ * app.
+ */
+ public final void startIntentSender(@NonNull IntentSender intentSender) {
+ if (mCallback == null) return;
+ try {
+ mCallback.onStartIntentSender(intentSender);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the metadata about the renderer. Returns {@code Bundle.Empty} if no metadata is
+ * provided.
+ */
+ @NonNull
+ public Bundle onGetInlineSuggestionsRendererInfo() {
+ return Bundle.EMPTY;
+ }
+
+ /**
+ * Renders the slice into a view.
+ */
+ @Nullable
+ public View onRenderSuggestion(@NonNull InlinePresentation presentation, int width,
+ int height) {
+ Log.e(TAG, "service implementation (" + getClass() + " does not implement "
+ + "onRenderSuggestion()");
+ return null;
+ }
+}
diff --git a/android/service/autofill/InlineSuggestionRoot.java b/android/service/autofill/InlineSuggestionRoot.java
new file mode 100644
index 0000000..c879653
--- /dev/null
+++ b/android/service/autofill/InlineSuggestionRoot.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 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.NonNull;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.MathUtils;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.widget.FrameLayout;
+
+/**
+ * This class is the root view for an inline suggestion. It is responsible for
+ * detecting the click on the item and to also transfer input focus to the IME
+ * window if we detect the user is scrolling.
+ *
+ * @hide
+ */
+@SuppressLint("ViewConstructor")
+public class InlineSuggestionRoot extends FrameLayout {
+ private static final String TAG = "InlineSuggestionRoot";
+
+ private final @NonNull IInlineSuggestionUiCallback mCallback;
+ private final int mTouchSlop;
+
+ private float mDownX;
+ private float mDownY;
+
+ public InlineSuggestionRoot(@NonNull Context context,
+ @NonNull IInlineSuggestionUiCallback callback) {
+ super(context);
+ mCallback = callback;
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ setFocusable(false);
+ }
+
+ @Override
+ @SuppressLint("ClickableViewAccessibility")
+ public boolean dispatchTouchEvent(@NonNull MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ mDownX = event.getX();
+ mDownY = event.getY();
+ } break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final float distance = MathUtils.dist(mDownX, mDownY,
+ event.getX(), event.getY());
+ final boolean isSecure = (event.getFlags()
+ & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) == 0;
+ if (!isSecure || distance > mTouchSlop) {
+ try {
+ mCallback.onTransferTouchFocusToImeWindow(getViewRootImpl().getInputToken(),
+ getContext().getDisplayId());
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException transferring touch focus to IME");
+ }
+ }
+ } break;
+ }
+ return super.dispatchTouchEvent(event);
+ }
+}
diff --git a/android/service/autofill/InternalOnClickAction.java b/android/service/autofill/InternalOnClickAction.java
new file mode 100644
index 0000000..6602f2d
--- /dev/null
+++ b/android/service/autofill/InternalOnClickAction.java
@@ -0,0 +1,36 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcelable;
+import android.view.ViewGroup;
+
+/**
+ * Superclass of all {@link OnClickAction} the system understands. As this is not public, all public
+ * subclasses have to implement {@link OnClickAction} again.
+ *
+ * @hide
+ */
+@TestApi
+public abstract class InternalOnClickAction implements OnClickAction, Parcelable {
+
+ /**
+ * Applies the action to the children of the {@code rootView} when clicked.
+ */
+ public abstract void onClick(@NonNull ViewGroup rootView);
+}
diff --git a/android/service/autofill/InternalSanitizer.java b/android/service/autofill/InternalSanitizer.java
new file mode 100644
index 0000000..ccffc70
--- /dev/null
+++ b/android/service/autofill/InternalSanitizer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcelable;
+import android.view.autofill.AutofillValue;
+
+/**
+ * Superclass of all sanitizers the system understands. As this is not public all public subclasses
+ * have to implement {@link Sanitizer} again.
+ *
+ * @hide
+ */
+@TestApi
+public abstract class InternalSanitizer implements Sanitizer, Parcelable {
+
+ /**
+ * Sanitizes an {@link AutofillValue}.
+ *
+ * @return sanitized value or {@code null} if value could not be sanitized (for example: didn't
+ * match regex, it's an invalid type, regex failed, etc).
+ */
+ @Nullable
+ public abstract AutofillValue sanitize(@NonNull AutofillValue value);
+}
diff --git a/android/service/autofill/InternalTransformation.java b/android/service/autofill/InternalTransformation.java
new file mode 100644
index 0000000..0dba2b9
--- /dev/null
+++ b/android/service/autofill/InternalTransformation.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcelable;
+import android.util.Log;
+import android.util.Pair;
+import android.widget.RemoteViews;
+
+import java.util.ArrayList;
+
+/**
+ * Superclass of all transformation the system understands. As this is not public all
+ * subclasses have to implement {@link Transformation} again.
+ *
+ * @hide
+ */
+@TestApi
+public abstract class InternalTransformation implements Transformation, Parcelable {
+
+ private static final String TAG = "InternalTransformation";
+
+ /**
+ * Applies this transformation to a child view of a {@link android.widget.RemoteViews
+ * presentation template}.
+ *
+ * @param finder object used to find the value of a field in the screen.
+ * @param template the {@link RemoteViews presentation template}.
+ * @param childViewId resource id of the child view inside the template.
+ */
+ abstract void apply(@NonNull ValueFinder finder, @NonNull RemoteViews template,
+ int childViewId) throws Exception;
+
+ /**
+ * Applies multiple transformations to the children views of a
+ * {@link android.widget.RemoteViews presentation template}.
+ *
+ * @param finder object used to find the value of a field in the screen.
+ * @param template the {@link RemoteViews presentation template}.
+ * @param transformations map of resource id of the child view inside the template to
+ * transformation.
+ */
+ public static boolean batchApply(@NonNull ValueFinder finder, @NonNull RemoteViews template,
+ @NonNull ArrayList<Pair<Integer, InternalTransformation>> transformations) {
+ final int size = transformations.size();
+ if (sDebug) Log.d(TAG, "getPresentation(): applying " + size + " transformations");
+ for (int i = 0; i < size; i++) {
+ final Pair<Integer, InternalTransformation> pair = transformations.get(i);
+ final int id = pair.first;
+ final InternalTransformation transformation = pair.second;
+ if (sDebug) Log.d(TAG, "#" + i + ": " + transformation);
+
+ try {
+ transformation.apply(finder, template, id);
+ } catch (Exception e) {
+ // Do not log full exception to avoid PII leaking
+ Log.e(TAG, "Could not apply transformation " + transformation + ": "
+ + e.getClass());
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/android/service/autofill/InternalValidator.java b/android/service/autofill/InternalValidator.java
new file mode 100644
index 0000000..4bea98d
--- /dev/null
+++ b/android/service/autofill/InternalValidator.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcelable;
+
+/**
+ * Superclass of all validators the system understands. As this is not public all public subclasses
+ * have to implement {@link Validator} again.
+ *
+ * @hide
+ */
+@TestApi
+public abstract class InternalValidator implements Validator, Parcelable {
+
+ /**
+ * Decides whether the contents of the screen are valid.
+ *
+ * @param finder object used to find the value of a field in the screen.
+ * @return {@code true} if the contents are valid, {@code false} otherwise.
+ */
+ public abstract boolean isValid(@NonNull ValueFinder finder);
+}
diff --git a/android/service/autofill/LuhnChecksumValidator.java b/android/service/autofill/LuhnChecksumValidator.java
new file mode 100644
index 0000000..ef0bd74
--- /dev/null
+++ b/android/service/autofill/LuhnChecksumValidator.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.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 com.android.internal.util.Preconditions;
+
+import java.util.Arrays;
+
+/**
+ * Validator that returns {@code true} if the number created by concatenating all given fields
+ * pass a Luhn algorithm checksum. All non-digits are ignored.
+ *
+ * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples.
+ */
+public final class LuhnChecksumValidator extends InternalValidator implements Validator,
+ Parcelable {
+ private static final String TAG = "LuhnChecksumValidator";
+
+ private final AutofillId[] mIds;
+
+ /**
+ * Default constructor.
+ *
+ * @param ids id of fields that comprises the number to be checked.
+ */
+ public LuhnChecksumValidator(@NonNull AutofillId... ids) {
+ mIds = Preconditions.checkArrayElementsNotNull(ids, "ids");
+ }
+
+ /**
+ * Checks if the Luhn checksum is valid.
+ *
+ * @param number The number including the checksum
+ */
+ private static boolean isLuhnChecksumValid(@NonNull String number) {
+ int sum = 0;
+ boolean isDoubled = false;
+
+ for (int i = number.length() - 1; i >= 0; i--) {
+ final int digit = number.charAt(i) - '0';
+ if (digit < 0 || digit > 9) {
+ // Ignore non-digits
+ continue;
+ }
+
+ int addend;
+ if (isDoubled) {
+ addend = digit * 2;
+ if (addend > 9) {
+ addend -= 9;
+ }
+ } else {
+ addend = digit;
+ }
+ sum += addend;
+ isDoubled = !isDoubled;
+ }
+
+ return sum % 10 == 0;
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public boolean isValid(@NonNull ValueFinder finder) {
+ if (mIds == null || mIds.length == 0) return false;
+
+ final StringBuilder builder = new StringBuilder();
+ for (AutofillId id : mIds) {
+ final String partialNumber = finder.findByAutofillId(id);
+ if (partialNumber == null) {
+ if (sDebug) Log.d(TAG, "No partial number for id " + id);
+ return false;
+ }
+ builder.append(partialNumber);
+ }
+
+ final String number = builder.toString();
+ boolean valid = isLuhnChecksumValid(number);
+ if (sDebug) Log.d(TAG, "isValid(" + number.length() + " chars): " + valid);
+ return valid;
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "LuhnChecksumValidator: [ids=" + Arrays.toString(mIds) + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelableArray(mIds, flags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<LuhnChecksumValidator> CREATOR =
+ new Parcelable.Creator<LuhnChecksumValidator>() {
+ @Override
+ public LuhnChecksumValidator createFromParcel(Parcel parcel) {
+ return new LuhnChecksumValidator(parcel.readParcelableArray(null, AutofillId.class));
+ }
+
+ @Override
+ public LuhnChecksumValidator[] newArray(int size) {
+ return new LuhnChecksumValidator[size];
+ }
+ };
+}
diff --git a/android/service/autofill/NegationValidator.java b/android/service/autofill/NegationValidator.java
new file mode 100644
index 0000000..2f098e2
--- /dev/null
+++ b/android/service/autofill/NegationValidator.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Validator used to implement a {@code NOT} logical operation.
+ *
+ * @hide
+ */
+final class NegationValidator extends InternalValidator {
+ @NonNull private final InternalValidator mValidator;
+
+ NegationValidator(@NonNull InternalValidator validator) {
+ mValidator = Preconditions.checkNotNull(validator);
+ }
+
+ @Override
+ public boolean isValid(@NonNull ValueFinder finder) {
+ return !mValidator.isValid(finder);
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "NegationValidator: [validator=" + mValidator + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mValidator, flags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<NegationValidator> CREATOR =
+ new Parcelable.Creator<NegationValidator>() {
+ @Override
+ public NegationValidator createFromParcel(Parcel parcel) {
+ return new NegationValidator(parcel.readParcelable(null));
+ }
+
+ @Override
+ public NegationValidator[] newArray(int size) {
+ return new NegationValidator[size];
+ }
+ };
+}
diff --git a/android/service/autofill/OnClickAction.java b/android/service/autofill/OnClickAction.java
new file mode 100644
index 0000000..8597a88
--- /dev/null
+++ b/android/service/autofill/OnClickAction.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/**
+ * Class used to define an action to be performed when a child view in a
+ * {@link android.widget.RemoteViews presentation} is clicked.
+ *
+ * <p>Typically used to switch the visibility of other views in a
+ * {@link CustomDescription custom save UI}.
+ *
+ * <p><b>Note:</b> This interface is not meant to be implemented by app developers; only
+ * implementations provided by the Android System can be used in other Autofill APIs.
+ */
+public interface OnClickAction {
+}
diff --git a/android/service/autofill/OptionalValidators.java b/android/service/autofill/OptionalValidators.java
new file mode 100644
index 0000000..7189c88
--- /dev/null
+++ b/android/service/autofill/OptionalValidators.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Compound validator that returns {@code true} on {@link #isValid(ValueFinder)} if any
+ * of its subvalidators returns {@code true} as well.
+ *
+ * <p>Used to implement an {@code OR} logical operation.
+ *
+ * @hide
+ */
+final class OptionalValidators extends InternalValidator {
+
+ private static final String TAG = "OptionalValidators";
+
+ @NonNull private final InternalValidator[] mValidators;
+
+ OptionalValidators(@NonNull InternalValidator[] validators) {
+ mValidators = Preconditions.checkArrayElementsNotNull(validators, "validators");
+ }
+
+ @Override
+ public boolean isValid(@NonNull ValueFinder finder) {
+ for (InternalValidator validator : mValidators) {
+ final boolean valid = validator.isValid(finder);
+ if (sDebug) Log.d(TAG, "isValid(" + validator + "): " + valid);
+ if (valid) return true;
+ }
+
+ return false;
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return new StringBuilder("OptionalValidators: [validators=").append(mValidators)
+ .append("]")
+ .toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelableArray(mValidators, flags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<OptionalValidators> CREATOR =
+ new Parcelable.Creator<OptionalValidators>() {
+ @Override
+ public OptionalValidators createFromParcel(Parcel parcel) {
+ return new OptionalValidators(parcel
+ .readParcelableArray(null, InternalValidator.class));
+ }
+
+ @Override
+ public OptionalValidators[] newArray(int size) {
+ return new OptionalValidators[size];
+ }
+ };
+}
diff --git a/android/service/autofill/RegexValidator.java b/android/service/autofill/RegexValidator.java
new file mode 100644
index 0000000..8cb67d0
--- /dev/null
+++ b/android/service/autofill/RegexValidator.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.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 com.android.internal.util.Preconditions;
+
+import java.util.regex.Pattern;
+
+/**
+ * Defines if a field is valid based on a regular expression (regex).
+ *
+ * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples.
+ */
+public final class RegexValidator extends InternalValidator implements Validator, Parcelable {
+
+ private static final String TAG = "RegexValidator";
+
+ private final AutofillId mId;
+ private final Pattern mRegex;
+
+ /**
+ * Default constructor.
+ *
+ * @param id id of the field whose regex is applied to.
+ * @param regex regular expression that defines the result of the validator: if the regex
+ * matches the contents of the field identified by {@code id}, it returns {@code true};
+ * otherwise, it returns {@code false}.
+ */
+ public RegexValidator(@NonNull AutofillId id, @NonNull Pattern regex) {
+ mId = Preconditions.checkNotNull(id);
+ mRegex = Preconditions.checkNotNull(regex);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public boolean isValid(@NonNull ValueFinder finder) {
+ final String value = finder.findByAutofillId(mId);
+ if (value == null) {
+ Log.w(TAG, "No view for id " + mId);
+ return false;
+ }
+
+ final boolean valid = mRegex.matcher(value).matches();
+ if (sDebug) Log.d(TAG, "isValid(): " + valid);
+ return valid;
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "RegexValidator: [id=" + mId + ", regex=" + mRegex + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mId, flags);
+ parcel.writeSerializable(mRegex);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<RegexValidator> CREATOR =
+ new Parcelable.Creator<RegexValidator>() {
+ @Override
+ public RegexValidator createFromParcel(Parcel parcel) {
+ return new RegexValidator(parcel.readParcelable(null),
+ (Pattern) parcel.readSerializable());
+ }
+
+ @Override
+ public RegexValidator[] newArray(int size) {
+ return new RegexValidator[size];
+ }
+ };
+}
diff --git a/android/service/autofill/RequiredValidators.java b/android/service/autofill/RequiredValidators.java
new file mode 100644
index 0000000..619eba0
--- /dev/null
+++ b/android/service/autofill/RequiredValidators.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Compound validator that only returns {@code true} on {@link #isValid(ValueFinder)} if all
+ * of its subvalidators return {@code true} as well.
+ *
+ * <p>Used to implement an {@code AND} logical operation.
+ *
+ * @hide
+ */
+final class RequiredValidators extends InternalValidator {
+
+ private static final String TAG = "RequiredValidators";
+
+ @NonNull private final InternalValidator[] mValidators;
+
+ RequiredValidators(@NonNull InternalValidator[] validators) {
+ mValidators = Preconditions.checkArrayElementsNotNull(validators, "validators");
+ }
+
+ @Override
+ public boolean isValid(@NonNull ValueFinder finder) {
+ for (InternalValidator validator : mValidators) {
+ final boolean valid = validator.isValid(finder);
+ if (sDebug) Log.d(TAG, "isValid(" + validator + "): " + valid);
+ if (!valid) return false;
+ }
+ return true;
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return new StringBuilder("RequiredValidators: [validators=").append(mValidators)
+ .append("]")
+ .toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelableArray(mValidators, flags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<RequiredValidators> CREATOR =
+ new Parcelable.Creator<RequiredValidators>() {
+ @Override
+ public RequiredValidators createFromParcel(Parcel parcel) {
+ return new RequiredValidators(parcel
+ .readParcelableArray(null, InternalValidator.class));
+ }
+
+ @Override
+ public RequiredValidators[] newArray(int size) {
+ return new RequiredValidators[size];
+ }
+ };
+}
diff --git a/android/service/autofill/Sanitizer.java b/android/service/autofill/Sanitizer.java
new file mode 100644
index 0000000..8a1310d
--- /dev/null
+++ b/android/service/autofill/Sanitizer.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/**
+ * Helper class used to sanitize user input before using it in a save request.
+ *
+ * <p>Typically used to avoid displaying the save UI for values that are autofilled but reformatted
+ * by the app—for example, if the autofill service sends a credit card number
+ * value as "004815162342108" and the app automatically changes it to "0048 1516 2342 108".
+ *
+ * <p><b>Note:</b> This interface is not meant to be implemented by app developers; only
+ * implementations provided by the Android System can be used in other Autofill APIs.
+ */
+public interface Sanitizer {
+}
diff --git a/android/service/autofill/SaveCallback.java b/android/service/autofill/SaveCallback.java
new file mode 100644
index 0000000..1753ecf
--- /dev/null
+++ b/android/service/autofill/SaveCallback.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.IntentSender;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Handles save requests from the {@link AutofillService} into the {@link Activity} being
+ * autofilled.
+ */
+public final class SaveCallback {
+
+ private static final String TAG = "SaveCallback";
+
+ private final ISaveCallback mCallback;
+ private boolean mCalled;
+
+ /** @hide */
+ SaveCallback(ISaveCallback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Notifies the Android System that an
+ * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} was successfully handled
+ * by the service.
+ *
+ * @throws IllegalStateException if this method, {@link #onSuccess(IntentSender)}, or
+ * {@link #onFailure(CharSequence)} was already called.
+ */
+ public void onSuccess() {
+ onSuccessInternal(null);
+ }
+
+ /**
+ * Notifies the Android System that an
+ * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} was successfully handled
+ * by the service.
+ *
+ * <p>This method is useful when the service requires extra work—for example, launching an
+ * activity asking the user to authenticate first —before it can process the request,
+ * as the intent will be launched from the context of the activity being autofilled and hence
+ * will be part of that activity's stack.
+ *
+ * @param intentSender intent that will be launched from the context of activity being
+ * autofilled.
+ *
+ * @throws IllegalStateException if this method, {@link #onSuccess()},
+ * or {@link #onFailure(CharSequence)} was already called.
+ */
+ public void onSuccess(@NonNull IntentSender intentSender) {
+ onSuccessInternal(Preconditions.checkNotNull(intentSender));
+ }
+
+ private void onSuccessInternal(@Nullable IntentSender intentSender) {
+ assertNotCalled();
+ mCalled = true;
+ try {
+ mCallback.onSuccess(intentSender);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+
+
+
+ /**
+ * Notifies the Android System that an
+ * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} could not be handled
+ * by the service.
+ *
+ * <p>This method is just used for logging purposes, the Android System won't call the service
+ * again in case of failures—if you need to recover from the failure, just save the
+ * {@link SaveRequest} and try again later.
+ *
+ * <p><b>Note: </b>for apps targeting {@link android.os.Build.VERSION_CODES#Q} or higher, this
+ * method just logs the message on {@code logcat}; for apps targetting older SDKs, it also
+ * displays the message to user using a {@link android.widget.Toast}.
+ *
+ * @param message error message. <b>Note: </b> this message should <b>not</b> contain PII
+ * (Personally Identifiable Information, such as username or email address).
+ *
+ * @throws IllegalStateException if this method, {@link #onSuccess()},
+ * or {@link #onSuccess(IntentSender)} was already called.
+ */
+ public void onFailure(CharSequence message) {
+ Log.w(TAG, "onFailure(): " + message);
+ assertNotCalled();
+ mCalled = true;
+ try {
+ mCallback.onFailure(message);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ private void assertNotCalled() {
+ if (mCalled) {
+ throw new IllegalStateException("Already called");
+ }
+ }
+}
diff --git a/android/service/autofill/SaveInfo.java b/android/service/autofill/SaveInfo.java
new file mode 100644
index 0000000..e640eec
--- /dev/null
+++ b/android/service/autofill/SaveInfo.java
@@ -0,0 +1,924 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.service.autofill.AutofillServiceHelper.assertValid;
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.IntentSender;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.DebugUtils;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * Information used to indicate that an {@link AutofillService} is interested on saving the
+ * user-inputed data for future use, through a
+ * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}
+ * call.
+ *
+ * <p>A {@link SaveInfo} is always associated with a {@link FillResponse}, and it contains at least
+ * two pieces of information:
+ *
+ * <ol>
+ * <li>The type(s) of user data (like password or credit card info) that would be saved.
+ * <li>The minimum set of views (represented by their {@link AutofillId}) that need to be changed
+ * to trigger a save request.
+ * </ol>
+ *
+ * <p>Typically, the {@link SaveInfo} contains the same {@code id}s as the {@link Dataset}:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder()
+ * .setValue(id1, AutofillValue.forText("homer"), createPresentation("homer")) // username
+ * .setValue(id2, AutofillValue.forText("D'OH!"), createPresentation("password for homer")) // password
+ * .build())
+ * .setSaveInfo(new SaveInfo.Builder(
+ * SaveInfo.SAVE_DATA_TYPE_USERNAME | SaveInfo.SAVE_DATA_TYPE_PASSWORD,
+ * new AutofillId[] { id1, id2 }).build())
+ * .build();
+ * </pre>
+ *
+ * <p>The save type flags are used to display the appropriate strings in the autofill save UI.
+ * You can pass multiple values, but try to keep it short if possible. In the above example, just
+ * {@code SaveInfo.SAVE_DATA_TYPE_PASSWORD} would be enough.
+ *
+ * <p>There might be cases where the {@link AutofillService} knows how to fill the screen,
+ * but the user has no data for it. In that case, the {@link FillResponse} should contain just the
+ * {@link SaveInfo}, but no {@link Dataset Datasets}:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD,
+ * new AutofillId[] { id1, id2 }).build())
+ * .build();
+ * </pre>
+ *
+ * <p>There might be cases where the user data in the {@link AutofillService} is enough
+ * to populate some fields but not all, and the service would still be interested on saving the
+ * other fields. In that case, the service could set the
+ * {@link SaveInfo.Builder#setOptionalIds(AutofillId[])} as well:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder()
+ * .setValue(id1, AutofillValue.forText("742 Evergreen Terrace"),
+ * createPresentation("742 Evergreen Terrace")) // street
+ * .setValue(id2, AutofillValue.forText("Springfield"),
+ * createPresentation("Springfield")) // city
+ * .build())
+ * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_ADDRESS,
+ * new AutofillId[] { id1, id2 }) // street and city
+ * .setOptionalIds(new AutofillId[] { id3, id4 }) // state and zipcode
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <a name="TriggeringSaveRequest"></a>
+ * <h3>Triggering a save request</h3>
+ *
+ * <p>The {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} can be triggered after
+ * any of the following events:
+ * <ul>
+ * <li>The {@link Activity} finishes.
+ * <li>The app explicitly calls {@link AutofillManager#commit()}.
+ * <li>All required views become invisible (if the {@link SaveInfo} was created with the
+ * {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} flag).
+ * <li>The user clicks a specific view (defined by {@link Builder#setTriggerId(AutofillId)}.
+ * </ul>
+ *
+ * <p>But it is only triggered when all conditions below are met:
+ * <ul>
+ * <li>The {@link SaveInfo} associated with the {@link FillResponse} is not {@code null} neither
+ * has the {@link #FLAG_DELAY_SAVE} flag.
+ * <li>The {@link AutofillValue}s of all required views (as set by the {@code requiredIds} passed
+ * to the {@link SaveInfo.Builder} constructor are not empty.
+ * <li>The {@link AutofillValue} of at least one view (be it required or optional) has changed
+ * (i.e., it's neither the same value passed in a {@link Dataset}, nor the initial value
+ * presented in the view).
+ * <li>There is no {@link Dataset} in the last {@link FillResponse} that completely matches the
+ * screen state (i.e., all required and optional fields in the dataset have the same value as
+ * the fields in the screen).
+ * <li>The user explicitly tapped the autofill save UI asking to save data for autofill.
+ * </ul>
+ *
+ * <a name="CustomizingSaveUI"></a>
+ * <h3>Customizing the autofill save UI</h3>
+ *
+ * <p>The service can also customize some aspects of the autofill save UI:
+ * <ul>
+ * <li>Add a simple subtitle by calling {@link Builder#setDescription(CharSequence)}.
+ * <li>Add a customized subtitle by calling
+ * {@link Builder#setCustomDescription(CustomDescription)}.
+ * <li>Customize the button used to reject the save request by calling
+ * {@link Builder#setNegativeAction(int, IntentSender)}.
+ * <li>Decide whether the UI should be shown based on the user input validation by calling
+ * {@link Builder#setValidator(Validator)}.
+ * </ul>
+ */
+public final class SaveInfo implements Parcelable {
+
+ /**
+ * Type used when the service can save the contents of a screen, but cannot describe what
+ * the content is for.
+ */
+ public static final int SAVE_DATA_TYPE_GENERIC = 0x0;
+
+ /**
+ * Type used when the {@link FillResponse} represents user credentials that have a password.
+ */
+ public static final int SAVE_DATA_TYPE_PASSWORD = 0x01;
+
+ /**
+ * Type used on when the {@link FillResponse} represents a physical address (such as street,
+ * city, state, etc).
+ */
+ public static final int SAVE_DATA_TYPE_ADDRESS = 0x02;
+
+ /**
+ * Type used when the {@link FillResponse} represents a credit card.
+ */
+ public static final int SAVE_DATA_TYPE_CREDIT_CARD = 0x04;
+
+ /**
+ * Type used when the {@link FillResponse} represents just an username, without a password.
+ */
+ public static final int SAVE_DATA_TYPE_USERNAME = 0x08;
+
+ /**
+ * Type used when the {@link FillResponse} represents just an email address, without a password.
+ */
+ public static final int SAVE_DATA_TYPE_EMAIL_ADDRESS = 0x10;
+
+ /**
+ * Type used when the {@link FillResponse} represents a debit card.
+ */
+ public static final int SAVE_DATA_TYPE_DEBIT_CARD = 0x20;
+
+ /**
+ * Type used when the {@link FillResponse} represents a payment card except for credit and
+ * debit cards.
+ */
+ public static final int SAVE_DATA_TYPE_PAYMENT_CARD = 0x40;
+
+ /**
+ * Type used when the {@link FillResponse} represents a card that does not a specified card or
+ * cannot identify what the card is for.
+ */
+ public static final int SAVE_DATA_TYPE_GENERIC_CARD = 0x80;
+
+ /**
+ * Style for the negative button of the save UI to cancel the
+ * save operation. In this case, the user tapping the negative
+ * button signals that they would prefer to not save the filled
+ * content.
+ */
+ public static final int NEGATIVE_BUTTON_STYLE_CANCEL = 0;
+
+ /**
+ * Style for the negative button of the save UI to reject the
+ * save operation. This could be useful if the user needs to
+ * opt-in your service and the save prompt is an advertisement
+ * of the potential value you can add to the user. In this
+ * case, the user tapping the negative button sends a strong
+ * signal that the feature may not be useful and you may
+ * consider some backoff strategy.
+ */
+ public static final int NEGATIVE_BUTTON_STYLE_REJECT = 1;
+
+ /**
+ * Style for the negative button of the save UI to never do the
+ * save operation. This means that the user does not need to save
+ * any data on this activity or application. Once the user tapping
+ * the negative button, the service should never trigger the save
+ * UI again. In addition to this, must consider providing restore
+ * options for the user.
+ */
+ public static final int NEGATIVE_BUTTON_STYLE_NEVER = 2;
+
+ /** @hide */
+ @IntDef(prefix = { "NEGATIVE_BUTTON_STYLE_" }, value = {
+ NEGATIVE_BUTTON_STYLE_CANCEL,
+ NEGATIVE_BUTTON_STYLE_REJECT,
+ NEGATIVE_BUTTON_STYLE_NEVER
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface NegativeButtonStyle{}
+
+ /**
+ * Style for the positive button of save UI to request the save operation.
+ * In this case, the user tapping the positive button signals that they
+ * agrees to save the filled content.
+ */
+ public static final int POSITIVE_BUTTON_STYLE_SAVE = 0;
+
+ /**
+ * Style for the positive button of save UI to have next action before the save operation.
+ * This could be useful if the filled content contains sensitive personally identifiable
+ * information and then requires user confirmation or verification. In this case, the user
+ * tapping the positive button signals that they would complete the next required action
+ * to save the filled content.
+ */
+ public static final int POSITIVE_BUTTON_STYLE_CONTINUE = 1;
+
+ /** @hide */
+ @IntDef(prefix = { "POSITIVE_BUTTON_STYLE_" }, value = {
+ POSITIVE_BUTTON_STYLE_SAVE,
+ POSITIVE_BUTTON_STYLE_CONTINUE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface PositiveButtonStyle{}
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "SAVE_DATA_TYPE_" }, value = {
+ SAVE_DATA_TYPE_GENERIC,
+ SAVE_DATA_TYPE_PASSWORD,
+ SAVE_DATA_TYPE_ADDRESS,
+ SAVE_DATA_TYPE_CREDIT_CARD,
+ SAVE_DATA_TYPE_USERNAME,
+ SAVE_DATA_TYPE_EMAIL_ADDRESS,
+ SAVE_DATA_TYPE_DEBIT_CARD,
+ SAVE_DATA_TYPE_PAYMENT_CARD,
+ SAVE_DATA_TYPE_GENERIC_CARD
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface SaveDataType{}
+
+ /**
+ * Usually, a save request is only automatically <a href="#TriggeringSaveRequest">triggered</a>
+ * once the {@link Activity} finishes. If this flag is set, it is triggered once all saved views
+ * become invisible.
+ */
+ public static final int FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE = 0x1;
+
+ /**
+ * By default, a save request is automatically <a href="#TriggeringSaveRequest">triggered</a>
+ * once the {@link Activity} finishes. If this flag is set, finishing the activity doesn't
+ * trigger a save request.
+ *
+ * <p>This flag is typically used in conjunction with {@link Builder#setTriggerId(AutofillId)}.
+ */
+ public static final int FLAG_DONT_SAVE_ON_FINISH = 0x2;
+
+
+ /**
+ * Postpone the autofill save UI.
+ *
+ * <p>If flag is set, the autofill save UI is not triggered when the
+ * autofill context associated with the response associated with this {@link SaveInfo} is
+ * committed (with {@link AutofillManager#commit()}). Instead, the {@link FillContext}
+ * is delivered in future fill requests (with {@link
+ * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)})
+ * and save request (with {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)})
+ * of an activity belonging to the same task.
+ *
+ * <p>This flag should be used when the service detects that the application uses
+ * multiple screens to implement an autofillable workflow (for example, one screen for the
+ * username field, another for password).
+ */
+ // TODO(b/113281366): improve documentation: add example, document relationship with other
+ // flagss, etc...
+ public static final int FLAG_DELAY_SAVE = 0x4;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE,
+ FLAG_DONT_SAVE_ON_FINISH,
+ FLAG_DELAY_SAVE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface SaveInfoFlags{}
+
+ private final @SaveDataType int mType;
+ private final @NegativeButtonStyle int mNegativeButtonStyle;
+ private final @PositiveButtonStyle int mPositiveButtonStyle;
+ private final IntentSender mNegativeActionListener;
+ private final AutofillId[] mRequiredIds;
+ private final AutofillId[] mOptionalIds;
+ private final CharSequence mDescription;
+ private final int mFlags;
+ private final CustomDescription mCustomDescription;
+ private final InternalValidator mValidator;
+ private final InternalSanitizer[] mSanitizerKeys;
+ private final AutofillId[][] mSanitizerValues;
+ private final AutofillId mTriggerId;
+
+ private SaveInfo(Builder builder) {
+ mType = builder.mType;
+ mNegativeButtonStyle = builder.mNegativeButtonStyle;
+ mNegativeActionListener = builder.mNegativeActionListener;
+ mPositiveButtonStyle = builder.mPositiveButtonStyle;
+ mRequiredIds = builder.mRequiredIds;
+ mOptionalIds = builder.mOptionalIds;
+ mDescription = builder.mDescription;
+ mFlags = builder.mFlags;
+ mCustomDescription = builder.mCustomDescription;
+ mValidator = builder.mValidator;
+ if (builder.mSanitizers == null) {
+ mSanitizerKeys = null;
+ mSanitizerValues = null;
+ } else {
+ final int size = builder.mSanitizers.size();
+ mSanitizerKeys = new InternalSanitizer[size];
+ mSanitizerValues = new AutofillId[size][];
+ for (int i = 0; i < size; i++) {
+ mSanitizerKeys[i] = builder.mSanitizers.keyAt(i);
+ mSanitizerValues[i] = builder.mSanitizers.valueAt(i);
+ }
+ }
+ mTriggerId = builder.mTriggerId;
+ }
+
+ /** @hide */
+ public @NegativeButtonStyle int getNegativeActionStyle() {
+ return mNegativeButtonStyle;
+ }
+
+ /** @hide */
+ public @Nullable IntentSender getNegativeActionListener() {
+ return mNegativeActionListener;
+ }
+
+ /** @hide */
+ public @PositiveButtonStyle int getPositiveActionStyle() {
+ return mPositiveButtonStyle;
+ }
+
+ /** @hide */
+ public @Nullable AutofillId[] getRequiredIds() {
+ return mRequiredIds;
+ }
+
+ /** @hide */
+ public @Nullable AutofillId[] getOptionalIds() {
+ return mOptionalIds;
+ }
+
+ /** @hide */
+ public @SaveDataType int getType() {
+ return mType;
+ }
+
+ /** @hide */
+ public @SaveInfoFlags int getFlags() {
+ return mFlags;
+ }
+
+ /** @hide */
+ public CharSequence getDescription() {
+ return mDescription;
+ }
+
+ /** @hide */
+ @Nullable
+ public CustomDescription getCustomDescription() {
+ return mCustomDescription;
+ }
+
+ /** @hide */
+ @Nullable
+ public InternalValidator getValidator() {
+ return mValidator;
+ }
+
+ /** @hide */
+ @Nullable
+ public InternalSanitizer[] getSanitizerKeys() {
+ return mSanitizerKeys;
+ }
+
+ /** @hide */
+ @Nullable
+ public AutofillId[][] getSanitizerValues() {
+ return mSanitizerValues;
+ }
+
+ /** @hide */
+ @Nullable
+ public AutofillId getTriggerId() {
+ return mTriggerId;
+ }
+
+ /**
+ * A builder for {@link SaveInfo} objects.
+ */
+ public static final class Builder {
+
+ private final @SaveDataType int mType;
+ private @NegativeButtonStyle int mNegativeButtonStyle = NEGATIVE_BUTTON_STYLE_CANCEL;
+ private @PositiveButtonStyle int mPositiveButtonStyle = POSITIVE_BUTTON_STYLE_SAVE;
+ private IntentSender mNegativeActionListener;
+ private final AutofillId[] mRequiredIds;
+ private AutofillId[] mOptionalIds;
+ private CharSequence mDescription;
+ private boolean mDestroyed;
+ private int mFlags;
+ private CustomDescription mCustomDescription;
+ private InternalValidator mValidator;
+ private ArrayMap<InternalSanitizer, AutofillId[]> mSanitizers;
+ // Set used to validate against duplicate ids.
+ private ArraySet<AutofillId> mSanitizerIds;
+ private AutofillId mTriggerId;
+
+ /**
+ * Creates a new builder.
+ *
+ * @param type the type of information the associated {@link FillResponse} represents. It
+ * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC},
+ * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_DEBIT_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_PAYMENT_CARD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_GENERIC_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_USERNAME},
+ * or {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}.
+ * @param requiredIds ids of all required views that will trigger a save request.
+ *
+ * <p>See {@link SaveInfo} for more info.
+ *
+ * @throws IllegalArgumentException if {@code requiredIds} is {@code null} or empty, or if
+ * it contains any {@code null} entry.
+ */
+ public Builder(@SaveDataType int type, @NonNull AutofillId[] requiredIds) {
+ mType = type;
+ mRequiredIds = assertValid(requiredIds);
+ }
+
+ /**
+ * Creates a new builder when no id is required.
+ *
+ * <p>When using this builder, caller must call {@link #setOptionalIds(AutofillId[])} before
+ * calling {@link #build()}.
+ *
+ * @param type the type of information the associated {@link FillResponse} represents. It
+ * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC},
+ * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_DEBIT_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_PAYMENT_CARD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_GENERIC_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_USERNAME},
+ * or {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}.
+ *
+ * <p>See {@link SaveInfo} for more info.
+ */
+ public Builder(@SaveDataType int type) {
+ mType = type;
+ mRequiredIds = null;
+ }
+
+ /**
+ * Sets flags changing the save behavior.
+ *
+ * @param flags {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE},
+ * {@link #FLAG_DONT_SAVE_ON_FINISH}, {@link #FLAG_DELAY_SAVE}, or {@code 0}.
+ * @return This builder.
+ */
+ public @NonNull Builder setFlags(@SaveInfoFlags int flags) {
+ throwIfDestroyed();
+
+ mFlags = Preconditions.checkFlagsArgument(flags,
+ FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE | FLAG_DONT_SAVE_ON_FINISH
+ | FLAG_DELAY_SAVE);
+ return this;
+ }
+
+ /**
+ * Sets the ids of additional, optional views the service would be interested to save.
+ *
+ * <p>See {@link SaveInfo} for more info.
+ *
+ * @param ids The ids of the optional views.
+ * @return This builder.
+ *
+ * @throws IllegalArgumentException if {@code ids} is {@code null} or empty, or if
+ * it contains any {@code null} entry.
+ */
+ public @NonNull Builder setOptionalIds(@NonNull AutofillId[] ids) {
+ throwIfDestroyed();
+ mOptionalIds = assertValid(ids);
+ return this;
+ }
+
+ /**
+ * Sets an optional description to be shown in the UI when the user is asked to save.
+ *
+ * <p>Typically, it describes how the data will be stored by the service, so it can help
+ * users to decide whether they can trust the service to save their data.
+ *
+ * @param description a succint description.
+ * @return This Builder.
+ *
+ * @throws IllegalStateException if this call was made after calling
+ * {@link #setCustomDescription(CustomDescription)}.
+ */
+ public @NonNull Builder setDescription(@Nullable CharSequence description) {
+ throwIfDestroyed();
+ Preconditions.checkState(mCustomDescription == null,
+ "Can call setDescription() or setCustomDescription(), but not both");
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Sets a custom description to be shown in the UI when the user is asked to save.
+ *
+ * <p>Typically used when the service must show more info about the object being saved,
+ * like a credit card logo, masked number, and expiration date.
+ *
+ * @param customDescription the custom description.
+ * @return This Builder.
+ *
+ * @throws IllegalStateException if this call was made after calling
+ * {@link #setDescription(CharSequence)}.
+ */
+ public @NonNull Builder setCustomDescription(@NonNull CustomDescription customDescription) {
+ throwIfDestroyed();
+ Preconditions.checkState(mDescription == null,
+ "Can call setDescription() or setCustomDescription(), but not both");
+ mCustomDescription = customDescription;
+ return this;
+ }
+
+ /**
+ * Sets the style and listener for the negative save action.
+ *
+ * <p>This allows an autofill service to customize the style and be
+ * notified when the user selects the negative action in the save
+ * UI. Note that selecting the negative action regardless of its style
+ * and listener being customized would dismiss the save UI and if a
+ * custom listener intent is provided then this intent is
+ * started. The default style is {@link #NEGATIVE_BUTTON_STYLE_CANCEL}</p>
+ *
+ * @param style The action style.
+ * @param listener The action listener.
+ * @return This builder.
+ *
+ * @see #NEGATIVE_BUTTON_STYLE_CANCEL
+ * @see #NEGATIVE_BUTTON_STYLE_REJECT
+ * @see #NEGATIVE_BUTTON_STYLE_NEVER
+ *
+ * @throws IllegalArgumentException If the style is invalid
+ */
+ public @NonNull Builder setNegativeAction(@NegativeButtonStyle int style,
+ @Nullable IntentSender listener) {
+ throwIfDestroyed();
+ Preconditions.checkArgumentInRange(style, NEGATIVE_BUTTON_STYLE_CANCEL,
+ NEGATIVE_BUTTON_STYLE_NEVER, "style");
+ mNegativeButtonStyle = style;
+ mNegativeActionListener = listener;
+ return this;
+ }
+
+ /**
+ * Sets the style for the positive save action.
+ *
+ * <p>This allows an autofill service to customize the style of the
+ * positive action in the save UI. Note that selecting the positive
+ * action regardless of its style would dismiss the save UI and calling
+ * into the {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback) save request}.
+ * The service should take the next action if selecting style
+ * {@link #POSITIVE_BUTTON_STYLE_CONTINUE}. The default style is
+ * {@link #POSITIVE_BUTTON_STYLE_SAVE}
+ *
+ * @param style The action style.
+ * @return This builder.
+ *
+ * @see #POSITIVE_BUTTON_STYLE_SAVE
+ * @see #POSITIVE_BUTTON_STYLE_CONTINUE
+ *
+ * @throws IllegalArgumentException If the style is invalid
+ */
+ public @NonNull Builder setPositiveAction(@PositiveButtonStyle int style) {
+ throwIfDestroyed();
+ Preconditions.checkArgumentInRange(style, POSITIVE_BUTTON_STYLE_SAVE,
+ POSITIVE_BUTTON_STYLE_CONTINUE, "style");
+ mPositiveButtonStyle = style;
+ return this;
+ }
+
+ /**
+ * Sets an object used to validate the user input - if the input is not valid, the
+ * autofill save UI is not shown.
+ *
+ * <p>Typically used to validate credit card numbers. Examples:
+ *
+ * <p>Validator for a credit number that must have exactly 16 digits:
+ *
+ * <pre class="prettyprint">
+ * Validator validator = new RegexValidator(ccNumberId, Pattern.compile(""^\\d{16}$"))
+ * </pre>
+ *
+ * <p>Validator for a credit number that must pass a Luhn checksum and either have
+ * 16 digits, or 15 digits starting with 108:
+ *
+ * <pre class="prettyprint">
+ * import static android.service.autofill.Validators.and;
+ * import static android.service.autofill.Validators.or;
+ *
+ * Validator validator =
+ * and(
+ * new LuhnChecksumValidator(ccNumberId),
+ * or(
+ * new RegexValidator(ccNumberId, Pattern.compile("^\\d{16}$")),
+ * new RegexValidator(ccNumberId, Pattern.compile("^108\\d{12}$"))
+ * )
+ * );
+ * </pre>
+ *
+ * <p><b>Note:</b> the example above is just for illustrative purposes; the same validator
+ * could be created using a single regex for the {@code OR} part:
+ *
+ * <pre class="prettyprint">
+ * Validator validator =
+ * and(
+ * new LuhnChecksumValidator(ccNumberId),
+ * new RegexValidator(ccNumberId, Pattern.compile(""^(\\d{16}|108\\d{12})$"))
+ * );
+ * </pre>
+ *
+ * <p>Validator for a credit number contained in just 4 fields and that must have exactly
+ * 4 digits on each field:
+ *
+ * <pre class="prettyprint">
+ * import static android.service.autofill.Validators.and;
+ *
+ * Validator validator =
+ * and(
+ * new RegexValidator(ccNumberId1, Pattern.compile("^\\d{4}$")),
+ * new RegexValidator(ccNumberId2, Pattern.compile("^\\d{4}$")),
+ * new RegexValidator(ccNumberId3, Pattern.compile("^\\d{4}$")),
+ * new RegexValidator(ccNumberId4, Pattern.compile("^\\d{4}$"))
+ * );
+ * </pre>
+ *
+ * @param validator an implementation provided by the Android System.
+ * @return this builder.
+ *
+ * @throws IllegalArgumentException if {@code validator} is not a class provided
+ * by the Android System.
+ */
+ public @NonNull Builder setValidator(@NonNull Validator validator) {
+ throwIfDestroyed();
+ Preconditions.checkArgument((validator instanceof InternalValidator),
+ "not provided by Android System: " + validator);
+ mValidator = (InternalValidator) validator;
+ return this;
+ }
+
+ /**
+ * Adds a sanitizer for one or more field.
+ *
+ * <p>When a sanitizer is set for a field, the {@link AutofillValue} is sent to the
+ * sanitizer before a save request is <a href="#TriggeringSaveRequest">triggered</a>.
+ *
+ * <p>Typically used to avoid displaying the save UI for values that are autofilled but
+ * reformattedby the app. For example, to remove spaces between every 4 digits of a
+ * credit card number:
+ *
+ * <pre class="prettyprint">
+ * builder.addSanitizer(new TextValueSanitizer(
+ * Pattern.compile("^(\\d{4})\\s?(\\d{4})\\s?(\\d{4})\\s?(\\d{4})$", "$1$2$3$4")),
+ * ccNumberId);
+ * </pre>
+ *
+ * <p>The same sanitizer can be reused to sanitize multiple fields. For example, to trim
+ * both the username and password fields:
+ *
+ * <pre class="prettyprint">
+ * builder.addSanitizer(
+ * new TextValueSanitizer(Pattern.compile("^\\s*(.*)\\s*$"), "$1"),
+ * usernameId, passwordId);
+ * </pre>
+ *
+ * <p>The sanitizer can also be used as an alternative for a
+ * {@link #setValidator(Validator) validator}. If any of the {@code ids} is a
+ * {@link #Builder(int, AutofillId[]) required id} and the {@code sanitizer} fails
+ * because of it, then the save UI is not shown.
+ *
+ * @param sanitizer an implementation provided by the Android System.
+ * @param ids id of fields whose value will be sanitized.
+ * @return this builder.
+ *
+ * @throws IllegalArgumentException if a sanitizer for any of the {@code ids} has already
+ * been added or if {@code ids} is empty.
+ */
+ public @NonNull Builder addSanitizer(@NonNull Sanitizer sanitizer,
+ @NonNull AutofillId... ids) {
+ throwIfDestroyed();
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(ids), "ids cannot be empty or null");
+ Preconditions.checkArgument((sanitizer instanceof InternalSanitizer),
+ "not provided by Android System: " + sanitizer);
+
+ if (mSanitizers == null) {
+ mSanitizers = new ArrayMap<>();
+ mSanitizerIds = new ArraySet<>(ids.length);
+ }
+
+ // Check for duplicates first.
+ for (AutofillId id : ids) {
+ Preconditions.checkArgument(!mSanitizerIds.contains(id), "already added %s", id);
+ mSanitizerIds.add(id);
+ }
+
+ mSanitizers.put((InternalSanitizer) sanitizer, ids);
+
+ return this;
+ }
+
+ /**
+ * Explicitly defines the view that should commit the autofill context when clicked.
+ *
+ * <p>Usually, the save request is only automatically
+ * <a href="#TriggeringSaveRequest">triggered</a> after the activity is
+ * finished or all relevant views become invisible, but there are scenarios where the
+ * autofill context is automatically commited too late
+ * —for example, when the activity manually clears the autofillable views when a
+ * button is tapped. This method can be used to trigger the autofill save UI earlier in
+ * these scenarios.
+ *
+ * <p><b>Note:</b> This method should only be used in scenarios where the automatic workflow
+ * is not enough, otherwise it could trigger the autofill save UI when it should not—
+ * for example, when the user entered invalid credentials for the autofillable views.
+ */
+ public @NonNull Builder setTriggerId(@NonNull AutofillId id) {
+ throwIfDestroyed();
+ mTriggerId = Preconditions.checkNotNull(id);
+ return this;
+ }
+
+ /**
+ * Builds a new {@link SaveInfo} instance.
+ *
+ * @throws IllegalStateException if no
+ * {@link #Builder(int, AutofillId[]) required ids},
+ * or {@link #setOptionalIds(AutofillId[]) optional ids}, or {@link #FLAG_DELAY_SAVE}
+ * were set
+ */
+ public SaveInfo build() {
+ throwIfDestroyed();
+ Preconditions.checkState(
+ !ArrayUtils.isEmpty(mRequiredIds) || !ArrayUtils.isEmpty(mOptionalIds)
+ || (mFlags & FLAG_DELAY_SAVE) != 0,
+ "must have at least one required or optional id or FLAG_DELAYED_SAVE");
+ mDestroyed = true;
+ return new SaveInfo(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ final StringBuilder builder = new StringBuilder("SaveInfo: [type=")
+ .append(DebugUtils.flagsToString(SaveInfo.class, "SAVE_DATA_TYPE_", mType))
+ .append(", requiredIds=").append(Arrays.toString(mRequiredIds))
+ .append(", negative style=").append(DebugUtils.flagsToString(SaveInfo.class,
+ "NEGATIVE_BUTTON_STYLE_", mNegativeButtonStyle))
+ .append(", positive style=").append(DebugUtils.flagsToString(SaveInfo.class,
+ "POSITIVE_BUTTON_STYLE_", mPositiveButtonStyle));
+ if (mOptionalIds != null) {
+ builder.append(", optionalIds=").append(Arrays.toString(mOptionalIds));
+ }
+ if (mDescription != null) {
+ builder.append(", description=").append(mDescription);
+ }
+ if (mFlags != 0) {
+ builder.append(", flags=").append(mFlags);
+ }
+ if (mCustomDescription != null) {
+ builder.append(", customDescription=").append(mCustomDescription);
+ }
+ if (mValidator != null) {
+ builder.append(", validator=").append(mValidator);
+ }
+ if (mSanitizerKeys != null) {
+ builder.append(", sanitizerKeys=").append(mSanitizerKeys.length);
+ }
+ if (mSanitizerValues != null) {
+ builder.append(", sanitizerValues=").append(mSanitizerValues.length);
+ }
+ if (mTriggerId != null) {
+ builder.append(", triggerId=").append(mTriggerId);
+ }
+
+ return builder.append("]").toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mType);
+ parcel.writeParcelableArray(mRequiredIds, flags);
+ parcel.writeParcelableArray(mOptionalIds, flags);
+ parcel.writeInt(mNegativeButtonStyle);
+ parcel.writeParcelable(mNegativeActionListener, flags);
+ parcel.writeInt(mPositiveButtonStyle);
+ parcel.writeCharSequence(mDescription);
+ parcel.writeParcelable(mCustomDescription, flags);
+ parcel.writeParcelable(mValidator, flags);
+ parcel.writeParcelableArray(mSanitizerKeys, flags);
+ if (mSanitizerKeys != null) {
+ for (int i = 0; i < mSanitizerValues.length; i++) {
+ parcel.writeParcelableArray(mSanitizerValues[i], flags);
+ }
+ }
+ parcel.writeParcelable(mTriggerId, flags);
+ parcel.writeInt(mFlags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<SaveInfo> CREATOR = new Parcelable.Creator<SaveInfo>() {
+ @Override
+ public SaveInfo createFromParcel(Parcel parcel) {
+
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final int type = parcel.readInt();
+ final AutofillId[] requiredIds = parcel.readParcelableArray(null, AutofillId.class);
+ final Builder builder = requiredIds != null
+ ? new Builder(type, requiredIds)
+ : new Builder(type);
+ final AutofillId[] optionalIds = parcel.readParcelableArray(null, AutofillId.class);
+ if (optionalIds != null) {
+ builder.setOptionalIds(optionalIds);
+ }
+
+ builder.setNegativeAction(parcel.readInt(), parcel.readParcelable(null));
+ builder.setPositiveAction(parcel.readInt());
+ builder.setDescription(parcel.readCharSequence());
+ final CustomDescription customDescripton = parcel.readParcelable(null);
+ if (customDescripton != null) {
+ builder.setCustomDescription(customDescripton);
+ }
+ final InternalValidator validator = parcel.readParcelable(null);
+ if (validator != null) {
+ builder.setValidator(validator);
+ }
+ final InternalSanitizer[] sanitizers =
+ parcel.readParcelableArray(null, InternalSanitizer.class);
+ if (sanitizers != null) {
+ final int size = sanitizers.length;
+ for (int i = 0; i < size; i++) {
+ final AutofillId[] autofillIds =
+ parcel.readParcelableArray(null, AutofillId.class);
+ builder.addSanitizer(sanitizers[i], autofillIds);
+ }
+ }
+ final AutofillId triggerId = parcel.readParcelable(null);
+ if (triggerId != null) {
+ builder.setTriggerId(triggerId);
+ }
+ builder.setFlags(parcel.readInt());
+ return builder.build();
+ }
+
+ @Override
+ public SaveInfo[] newArray(int size) {
+ return new SaveInfo[size];
+ }
+ };
+}
diff --git a/android/service/autofill/SaveRequest.java b/android/service/autofill/SaveRequest.java
new file mode 100644
index 0000000..5dd07c4
--- /dev/null
+++ b/android/service/autofill/SaveRequest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents a request to an {@link AutofillService
+ * autofill provider} to save applicable data entered by the user.
+ *
+ * @see AutofillService#onSaveRequest(SaveRequest, SaveCallback)
+ */
+public final class SaveRequest implements Parcelable {
+ private final @NonNull ArrayList<FillContext> mFillContexts;
+ private final @Nullable Bundle mClientState;
+ private final @Nullable ArrayList<String> mDatasetIds;
+
+ /** @hide */
+ public SaveRequest(@NonNull ArrayList<FillContext> fillContexts,
+ @Nullable Bundle clientState, @Nullable ArrayList<String> datasetIds) {
+ mFillContexts = Preconditions.checkNotNull(fillContexts, "fillContexts");
+ mClientState = clientState;
+ mDatasetIds = datasetIds;
+ }
+
+ private SaveRequest(@NonNull Parcel parcel) {
+ this(parcel.createTypedArrayList(FillContext.CREATOR),
+ parcel.readBundle(), parcel.createStringArrayList());
+ }
+
+ /**
+ * Gets the contexts associated with each previous fill request.
+ *
+ * <p><b>Note:</b> Starting on Android {@link android.os.Build.VERSION_CODES#Q}, it could also
+ * include contexts from requests whose {@link SaveInfo} had the
+ * {@link SaveInfo#FLAG_DELAY_SAVE} flag.
+ *
+ * @return The contexts associated with each previous fill request.
+ */
+ public @NonNull List<FillContext> getFillContexts() {
+ return mFillContexts;
+ }
+
+ /**
+ * Gets the latest client state bundle set by the service in a
+ * {@link FillResponse.Builder#setClientState(Bundle) fill response}.
+ *
+ * <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state
+ * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On
+ * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of
+ * an authenticated request through the
+ * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are
+ * also considered (and take precedence when set).
+ *
+ * @return The client state.
+ */
+ public @Nullable Bundle getClientState() {
+ return mClientState;
+ }
+
+ /**
+ * Gets the ids of the datasets selected by the user, in the order in which they were selected.
+ */
+ @Nullable
+ public List<String> getDatasetIds() {
+ return mDatasetIds;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeTypedList(mFillContexts, flags);
+ parcel.writeBundle(mClientState);
+ parcel.writeStringList(mDatasetIds);
+ }
+
+ public static final @android.annotation.NonNull Creator<SaveRequest> CREATOR =
+ new Creator<SaveRequest>() {
+ @Override
+ public SaveRequest createFromParcel(Parcel parcel) {
+ return new SaveRequest(parcel);
+ }
+
+ @Override
+ public SaveRequest[] newArray(int size) {
+ return new SaveRequest[size];
+ }
+ };
+}
diff --git a/android/service/autofill/TextValueSanitizer.java b/android/service/autofill/TextValueSanitizer.java
new file mode 100644
index 0000000..cc48fcb
--- /dev/null
+++ b/android/service/autofill/TextValueSanitizer.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Slog;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Sanitizes a text {@link AutofillValue} using a regular expression (regex) substitution.
+ *
+ * <p>For example, to remove spaces from groups of 4-digits in a credit card:
+ *
+ * <pre class="prettyprint">
+ * new TextValueSanitizer(Pattern.compile("^(\\d{4})\\s?(\\d{4})\\s?(\\d{4})\\s?(\\d{4})$"),
+ * "$1$2$3$4")
+ * </pre>
+ */
+public final class TextValueSanitizer extends InternalSanitizer implements
+ Sanitizer, Parcelable {
+ private static final String TAG = "TextValueSanitizer";
+
+ private final Pattern mRegex;
+ private final String mSubst;
+
+ /**
+ * Default constructor.
+ *
+ * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
+ * are used to substitute parts of the {@link AutofillValue#getTextValue() text value}.
+ * @param subst the string that substitutes the matched regex, using {@code $} for
+ * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
+ */
+ public TextValueSanitizer(@NonNull Pattern regex, @NonNull String subst) {
+ mRegex = Preconditions.checkNotNull(regex);
+ mSubst = Preconditions.checkNotNull(subst);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ @Nullable
+ public AutofillValue sanitize(@NonNull AutofillValue value) {
+ if (value == null) {
+ Slog.w(TAG, "sanitize() called with null value");
+ return null;
+ }
+ if (!value.isText()) {
+ if (sDebug) Slog.d(TAG, "sanitize() called with non-text value: " + value);
+ return null;
+ }
+
+ final CharSequence text = value.getTextValue();
+
+ try {
+ final Matcher matcher = mRegex.matcher(text);
+ if (!matcher.matches()) {
+ if (sDebug) Slog.d(TAG, "sanitize(): " + mRegex + " failed for " + value);
+ return null;
+ }
+
+ final CharSequence sanitized = matcher.replaceAll(mSubst);
+ return AutofillValue.forText(sanitized);
+ } catch (Exception e) {
+ Slog.w(TAG, "Exception evaluating " + mRegex + "/" + mSubst + ": " + e);
+ return null;
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "TextValueSanitizer: [regex=" + mRegex + ", subst=" + mSubst + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeSerializable(mRegex);
+ parcel.writeString(mSubst);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<TextValueSanitizer> CREATOR =
+ new Parcelable.Creator<TextValueSanitizer>() {
+ @Override
+ public TextValueSanitizer createFromParcel(Parcel parcel) {
+ return new TextValueSanitizer((Pattern) parcel.readSerializable(), parcel.readString());
+ }
+
+ @Override
+ public TextValueSanitizer[] newArray(int size) {
+ return new TextValueSanitizer[size];
+ }
+ };
+}
diff --git a/android/service/autofill/Transformation.java b/android/service/autofill/Transformation.java
new file mode 100644
index 0000000..de43955
--- /dev/null
+++ b/android/service/autofill/Transformation.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+/**
+ * Helper class used to change a child view of a {@link android.widget.RemoteViews presentation
+ * template} at runtime, using the values of fields contained in the screen.
+ *
+ * <p>Typically used by {@link CustomDescription} to provide a customized autofill save UI.
+ *
+ * <p><b>Note:</b> This interface is not meant to be implemented by app developers; only
+ * implementations provided by the Android System can be used in other Autofill APIs.
+ */
+public interface Transformation {
+}
diff --git a/android/service/autofill/UserData.java b/android/service/autofill/UserData.java
new file mode 100644
index 0000000..7814f70
--- /dev/null
+++ b/android/service/autofill/UserData.java
@@ -0,0 +1,537 @@
+/*
+ * 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_CATEGORY_COUNT;
+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.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.Settings;
+import android.service.autofill.FieldClassification.Match;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.Helper;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Defines the user data used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ */
+public final class UserData implements FieldClassificationUserData, Parcelable {
+
+ private static final String TAG = "UserData";
+
+ private static final int DEFAULT_MAX_USER_DATA_SIZE = 50;
+ private static final int DEFAULT_MAX_CATEGORY_COUNT = 10;
+ private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10;
+ private static final int DEFAULT_MIN_VALUE_LENGTH = 3;
+ private static final int DEFAULT_MAX_VALUE_LENGTH = 100;
+
+ private final String mId;
+ private final String[] mCategoryIds;
+ private final String[] mValues;
+
+ private final String mDefaultAlgorithm;
+ private final Bundle mDefaultArgs;
+ private final ArrayMap<String, String> mCategoryAlgorithms;
+ private final ArrayMap<String, Bundle> mCategoryArgs;
+
+ private UserData(Builder builder) {
+ mId = builder.mId;
+ mCategoryIds = new String[builder.mCategoryIds.size()];
+ builder.mCategoryIds.toArray(mCategoryIds);
+ mValues = new String[builder.mValues.size()];
+ builder.mValues.toArray(mValues);
+ builder.mValues.toArray(mValues);
+
+ mDefaultAlgorithm = builder.mDefaultAlgorithm;
+ mDefaultArgs = builder.mDefaultArgs;
+ mCategoryAlgorithms = builder.mCategoryAlgorithms;
+ mCategoryArgs = builder.mCategoryArgs;
+ }
+
+ /**
+ * Gets the name of the default algorithm that is used to calculate
+ * {@link Match#getScore()} match scores}.
+ */
+ @Nullable
+ @Override
+ public String getFieldClassificationAlgorithm() {
+ return mDefaultAlgorithm;
+ }
+
+ /** @hide */
+ @Override
+ public Bundle getDefaultFieldClassificationArgs() {
+ return mDefaultArgs;
+ }
+
+ /**
+ * Gets the name of the algorithm corresponding to the specific autofill category
+ * that is used to calculate {@link Match#getScore() match scores}
+ *
+ * @param categoryId autofill field category
+ *
+ * @return String name of algorithm, null if none found.
+ */
+ @Nullable
+ @Override
+ public String getFieldClassificationAlgorithmForCategory(@NonNull String categoryId) {
+ Preconditions.checkNotNull(categoryId);
+ if (mCategoryAlgorithms == null || !mCategoryAlgorithms.containsKey(categoryId)) {
+ return null;
+ }
+ return mCategoryAlgorithms.get(categoryId);
+ }
+
+ /**
+ * Gets the id.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /** @hide */
+ @Override
+ public String[] getCategoryIds() {
+ return mCategoryIds;
+ }
+
+ /** @hide */
+ @Override
+ public String[] getValues() {
+ return mValues;
+ }
+
+ /** @hide */
+ @TestApi
+ @Override
+ public ArrayMap<String, String> getFieldClassificationAlgorithms() {
+ return mCategoryAlgorithms;
+ }
+
+ /** @hide */
+ @Override
+ public ArrayMap<String, Bundle> getFieldClassificationArgs() {
+ return mCategoryArgs;
+ }
+
+ /** @hide */
+ public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("id: "); pw.print(mId);
+ pw.print(prefix); pw.print("Default Algorithm: "); pw.print(mDefaultAlgorithm);
+ pw.print(prefix); pw.print("Default Args"); pw.print(mDefaultArgs);
+ if (mCategoryAlgorithms != null && mCategoryAlgorithms.size() > 0) {
+ pw.print(prefix); pw.print("Algorithms per category: ");
+ for (int i = 0; i < mCategoryAlgorithms.size(); i++) {
+ pw.print(prefix); pw.print(prefix); pw.print(mCategoryAlgorithms.keyAt(i));
+ pw.print(": "); pw.println(Helper.getRedacted(mCategoryAlgorithms.valueAt(i)));
+ pw.print("args="); pw.print(mCategoryArgs.get(mCategoryAlgorithms.keyAt(i)));
+ }
+ }
+ // Cannot disclose field ids or values because they could contain PII
+ pw.print(prefix); pw.print("Field ids size: "); pw.println(mCategoryIds.length);
+ for (int i = 0; i < mCategoryIds.length; i++) {
+ pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
+ pw.println(Helper.getRedacted(mCategoryIds[i]));
+ }
+ pw.print(prefix); pw.print("Values size: "); pw.println(mValues.length);
+ for (int i = 0; i < mValues.length; i++) {
+ pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
+ pw.println(Helper.getRedacted(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("maxCategoryCount: "); pw.println(getMaxCategoryCount());
+ pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength());
+ pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength());
+ }
+
+ /**
+ * A builder for {@link UserData} objects.
+ */
+ public static final class Builder {
+ private final String mId;
+ private final ArrayList<String> mCategoryIds;
+ private final ArrayList<String> mValues;
+ private String mDefaultAlgorithm;
+ private Bundle mDefaultArgs;
+
+ // Map of autofill field categories to fleid classification algorithms and args
+ private ArrayMap<String, String> mCategoryAlgorithms;
+ private ArrayMap<String, Bundle> mCategoryArgs;
+
+ private boolean mDestroyed;
+
+ // Non-persistent array used to limit the number of unique ids.
+ private final ArraySet<String> mUniqueCategoryIds;
+ // Non-persistent array used to ignore duplaicated value/category pairs.
+ private final ArraySet<String> mUniqueValueCategoryPairs;
+
+ /**
+ * Creates a new builder for the user data used for <a href="#FieldClassification">field
+ * classification</a>.
+ *
+ * <p>The user data must contain at least one pair of {@code value} -> {@code categoryId},
+ * and more pairs can be added through the {@link #add(String, String)} method. For example:
+ *
+ * <pre class="prettyprint">
+ * new UserData.Builder("v1", "Bart Simpson", "name")
+ * .add("[email protected]", "email")
+ * .add("[email protected]", "email")
+ * .build();
+ * </pre>
+ *
+ * @param id id used to identify the whole {@link UserData} object. This id is also returned
+ * by {@link AutofillManager#getUserDataId()}, which can be used to check if the
+ * {@link UserData} is up-to-date without fetching the whole object (through
+ * {@link AutofillManager#getUserData()}).
+ *
+ * @param value value of the user data.
+ * @param categoryId autofill field category.
+ *
+ * @throws IllegalArgumentException if any of the following occurs:
+ * <ul>
+ * <li>{@code id} is empty</li>
+ * <li>{@code categoryId} is empty</li>
+ * <li>{@code value} is empty</li>
+ * <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}</li>
+ * <li>the length of {@code value} is higher than
+ * {@link UserData#getMaxValueLength()}</li>
+ * </ul>
+ */
+ public Builder(@NonNull String id, @NonNull String value, @NonNull String categoryId) {
+ mId = checkNotEmpty("id", id);
+ checkNotEmpty("categoryId", categoryId);
+ checkValidValue(value);
+ final int maxUserDataSize = getMaxUserDataSize();
+ mCategoryIds = new ArrayList<>(maxUserDataSize);
+ mValues = new ArrayList<>(maxUserDataSize);
+ mUniqueValueCategoryPairs = new ArraySet<>(maxUserDataSize);
+
+ mUniqueCategoryIds = new ArraySet<>(getMaxCategoryCount());
+
+ addMapping(value, categoryId);
+ }
+
+ /**
+ * Sets the default algorithm used for
+ * <a href="#FieldClassification">field classification</a>.
+ *
+ * <p>The currently available algorithms can be retrieve through
+ * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
+ *
+ * <p>If not set, the
+ * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
+ * used instead.
+ *
+ * @param name name of the algorithm or {@code null} to used default.
+ * @param args optional arguments to the algorithm.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setFieldClassificationAlgorithm(@Nullable String name,
+ @Nullable Bundle args) {
+ throwIfDestroyed();
+ mDefaultAlgorithm = name;
+ mDefaultArgs = args;
+ return this;
+ }
+
+ /**
+ * Sets the algorithm used for <a href="#FieldClassification">field classification</a>
+ * for the specified category.
+ *
+ * <p>The currently available algorithms can be retrieved through
+ * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
+ *
+ * <p>If not set, the
+ * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
+ * used instead.
+ *
+ * @param categoryId autofill field category.
+ * @param name name of the algorithm or {@code null} to used default.
+ * @param args optional arguments to the algorithm.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setFieldClassificationAlgorithmForCategory(@NonNull String categoryId,
+ @Nullable String name, @Nullable Bundle args) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(categoryId);
+ if (mCategoryAlgorithms == null) {
+ mCategoryAlgorithms = new ArrayMap<>(getMaxCategoryCount());
+ }
+ if (mCategoryArgs == null) {
+ mCategoryArgs = new ArrayMap<>(getMaxCategoryCount());
+ }
+ mCategoryAlgorithms.put(categoryId, name);
+ mCategoryArgs.put(categoryId, args);
+ return this;
+ }
+
+ /**
+ * Adds a new value for user data.
+ *
+ * @param value value of the user data.
+ * @param categoryId string used to identify the category the value is associated with.
+ *
+ * @throws IllegalStateException if:
+ * <ul>
+ * <li>{@link #build()} already called</li>
+ * <li>the {@code value} has already been added (<b>Note: </b> this restriction was
+ * lifted on Android {@link android.os.Build.VERSION_CODES#Q} and later)</li>
+ * <li>the number of unique {@code categoryId} values added so far is more than
+ * {@link UserData#getMaxCategoryCount()}</li>
+ * <li>the number of {@code values} added so far is is more than
+ * {@link UserData#getMaxUserDataSize()}</li>
+ * </ul>
+ *
+ * @throws IllegalArgumentException if any of the following occurs:
+ * <ul>
+ * <li>{@code id} is empty</li>
+ * <li>{@code categoryId} is empty</li>
+ * <li>{@code value} is empty</li>
+ * <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}</li>
+ * <li>the length of {@code value} is higher than
+ * {@link UserData#getMaxValueLength()}</li>
+ * </ul>
+ */
+ @NonNull
+ public Builder add(@NonNull String value, @NonNull String categoryId) {
+ throwIfDestroyed();
+ checkNotEmpty("categoryId", categoryId);
+ checkValidValue(value);
+
+ if (!mUniqueCategoryIds.contains(categoryId)) {
+ // New category - check size
+ Preconditions.checkState(mUniqueCategoryIds.size() < getMaxCategoryCount(),
+ "already added " + mUniqueCategoryIds.size() + " unique category ids");
+ }
+
+ Preconditions.checkState(mValues.size() < getMaxUserDataSize(),
+ "already added " + mValues.size() + " elements");
+ addMapping(value, categoryId);
+
+ return this;
+ }
+
+ private void addMapping(@NonNull String value, @NonNull String categoryId) {
+ final String pair = value + ":" + categoryId;
+ if (mUniqueValueCategoryPairs.contains(pair)) {
+ // Don't include value on message because it could contain PII
+ Log.w(TAG, "Ignoring entry with same value / category");
+ return;
+ }
+ mCategoryIds.add(categoryId);
+ mValues.add(value);
+ mUniqueCategoryIds.add(categoryId);
+ mUniqueValueCategoryPairs.add(pair);
+ }
+
+ private String checkNotEmpty(@NonNull String name, @Nullable String value) {
+ Preconditions.checkNotNull(value);
+ Preconditions.checkArgument(!TextUtils.isEmpty(value), "%s cannot be empty", name);
+ return value;
+ }
+
+ 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.
+ */
+ @NonNull
+ 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();
+
+ final StringBuilder builder = new StringBuilder("UserData: [id=").append(mId);
+ // Cannot disclose category ids or values because they could contain PII
+ builder.append(", categoryIds=");
+ Helper.appendRedacted(builder, mCategoryIds);
+ 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.writeString(mId);
+ parcel.writeStringArray(mCategoryIds);
+ parcel.writeStringArray(mValues);
+ parcel.writeString(mDefaultAlgorithm);
+ parcel.writeBundle(mDefaultArgs);
+ parcel.writeMap(mCategoryAlgorithms);
+ parcel.writeMap(mCategoryArgs);
+ }
+
+ public static final @android.annotation.NonNull 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 id = parcel.readString();
+ final String[] categoryIds = parcel.readStringArray();
+ final String[] values = parcel.readStringArray();
+ final String defaultAlgorithm = parcel.readString();
+ final Bundle defaultArgs = parcel.readBundle();
+ final ArrayMap<String, String> categoryAlgorithms = new ArrayMap<>();
+ parcel.readMap(categoryAlgorithms, String.class.getClassLoader());
+ final ArrayMap<String, Bundle> categoryArgs = new ArrayMap<>();
+ parcel.readMap(categoryArgs, Bundle.class.getClassLoader());
+
+ final Builder builder = new Builder(id, values[0], categoryIds[0])
+ .setFieldClassificationAlgorithm(defaultAlgorithm, defaultArgs);
+
+ for (int i = 1; i < categoryIds.length; i++) {
+ String categoryId = categoryIds[i];
+ builder.add(values[i], categoryId);
+ }
+
+ final int size = categoryAlgorithms.size();
+ if (size > 0) {
+ for (int i = 0; i < size; i++) {
+ final String categoryId = categoryAlgorithms.keyAt(i);
+ builder.setFieldClassificationAlgorithmForCategory(categoryId,
+ categoryAlgorithms.valueAt(i), categoryArgs.get(categoryId));
+ }
+ }
+ 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 maximum number of unique category ids that can be passed to
+ * the builder's constructor and {@link Builder#add(String, String)}.
+ */
+ public static int getMaxCategoryCount() {
+ return getInt(AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, DEFAULT_MAX_CATEGORY_COUNT);
+ }
+
+ /**
+ * Gets the minimum length of values passed to the builder's constructor or
+ * or {@link Builder#add(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 the builder's constructor or
+ * or {@link Builder#add(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/android/service/autofill/Validator.java b/android/service/autofill/Validator.java
new file mode 100644
index 0000000..a4036f2
--- /dev/null
+++ b/android/service/autofill/Validator.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+/**
+ * Class used to define whether a condition is satisfied.
+ *
+ * <p>Typically used to avoid displaying the save UI when the user input is invalid.
+ */
+public interface Validator {
+}
diff --git a/android/service/autofill/Validators.java b/android/service/autofill/Validators.java
new file mode 100644
index 0000000..0f1ba98
--- /dev/null
+++ b/android/service/autofill/Validators.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+import android.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Factory for {@link Validator} operations.
+ *
+ * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples.
+ */
+public final class Validators {
+
+ private Validators() {
+ throw new UnsupportedOperationException("contains static methods only");
+ }
+
+ /**
+ * Creates a validator that is only valid if all {@code validators} are valid.
+ *
+ * <p>Used to represent an {@code AND} boolean operation in a chain of validators.
+ *
+ * @throws IllegalArgumentException if any element of {@code validators} is an instance of a
+ * class that is not provided by the Android System.
+ */
+ @NonNull
+ public static Validator and(@NonNull Validator...validators) {
+ return new RequiredValidators(getInternalValidators(validators));
+ }
+
+ /**
+ * Creates a validator that is valid if any of the {@code validators} is valid.
+ *
+ * <p>Used to represent an {@code OR} boolean operation in a chain of validators.
+ *
+ * @throws IllegalArgumentException if any element of {@code validators} is an instance of a
+ * class that is not provided by the Android System.
+ */
+ @NonNull
+ public static Validator or(@NonNull Validator...validators) {
+ return new OptionalValidators(getInternalValidators(validators));
+ }
+
+ /**
+ * Creates a validator that is valid when {@code validator} is not, and vice versa.
+ *
+ * <p>Used to represent a {@code NOT} boolean operation in a chain of validators.
+ *
+ * @throws IllegalArgumentException if {@code validator} is an instance of a class that is not
+ * provided by the Android System.
+ */
+ @NonNull
+ public static Validator not(@NonNull Validator validator) {
+ Preconditions.checkArgument(validator instanceof InternalValidator,
+ "validator not provided by Android System: " + validator);
+ return new NegationValidator((InternalValidator) validator);
+ }
+
+ private static InternalValidator[] getInternalValidators(Validator[] validators) {
+ Preconditions.checkArrayElementsNotNull(validators, "validators");
+
+ final InternalValidator[] internals = new InternalValidator[validators.length];
+
+ for (int i = 0; i < validators.length; i++) {
+ Preconditions.checkArgument((validators[i] instanceof InternalValidator),
+ "element " + i + " not provided by Android System: " + validators[i]);
+ internals[i] = (InternalValidator) validators[i];
+ }
+ return internals;
+ }
+}
diff --git a/android/service/autofill/ValueFinder.java b/android/service/autofill/ValueFinder.java
new file mode 100644
index 0000000..7f195d6
--- /dev/null
+++ b/android/service/autofill/ValueFinder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+import android.annotation.NonNull;
+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.
+ *
+ * @hide
+ */
+@TestApi
+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
+ AutofillValue findRawValueByAutofillId(@NonNull AutofillId id);
+}
diff --git a/android/service/autofill/VisibilitySetterAction.java b/android/service/autofill/VisibilitySetterAction.java
new file mode 100644
index 0000000..e29a23f
--- /dev/null
+++ b/android/service/autofill/VisibilitySetterAction.java
@@ -0,0 +1,178 @@
+/*
+ * 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 static android.view.autofill.Helper.sVerbose;
+
+import android.annotation.IdRes;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Slog;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.View.Visibility;
+import android.view.ViewGroup;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Action used to change the visibility of other child view in a {@link CustomDescription}
+ * {@link RemoteViews presentation template}.
+ *
+ * <p>See {@link CustomDescription.Builder#addOnClickAction(int, OnClickAction)} for more details.
+ */
+public final class VisibilitySetterAction extends InternalOnClickAction implements
+ OnClickAction, Parcelable {
+ private static final String TAG = "VisibilitySetterAction";
+
+ @NonNull private final SparseIntArray mVisibilities;
+
+ private VisibilitySetterAction(@NonNull Builder builder) {
+ mVisibilities = builder.mVisibilities;
+ }
+
+ /** @hide */
+ @Override
+ public void onClick(@NonNull ViewGroup rootView) {
+ for (int i = 0; i < mVisibilities.size(); i++) {
+ final int id = mVisibilities.keyAt(i);
+ final View child = rootView.findViewById(id);
+ if (child == null) {
+ Slog.w(TAG, "Skipping view id " + id + " because it's not found on " + rootView);
+ continue;
+ }
+ final int visibility = mVisibilities.valueAt(i);
+ if (sVerbose) {
+ Slog.v(TAG, "Changing visibility of view " + child + " from "
+ + child.getVisibility() + " to " + visibility);
+ }
+ child.setVisibility(visibility);
+ }
+ }
+
+ /**
+ * Builder for {@link VisibilitySetterAction} objects.
+ */
+ public static final class Builder {
+ private final SparseIntArray mVisibilities = new SparseIntArray();
+ private boolean mDestroyed;
+
+ /**
+ * Creates a new builder for an action that change the visibility of one child view.
+ *
+ * @param id view resource id of the children view.
+ * @param visibility one of {@link View#VISIBLE}, {@link View#INVISIBLE}, or
+ * {@link View#GONE}.
+ * @throws IllegalArgumentException if visibility is not one of {@link View#VISIBLE},
+ * {@link View#INVISIBLE}, or {@link View#GONE}.
+ */
+ public Builder(@IdRes int id, @Visibility int visibility) {
+ setVisibility(id, visibility);
+ }
+
+ /**
+ * Sets the action to changes the visibility of a child view.
+ *
+ * @param id view resource id of the children view.
+ * @param visibility one of {@link View#VISIBLE}, {@link View#INVISIBLE}, or
+ * {@link View#GONE}.
+ * @throws IllegalArgumentException if visibility is not one of {@link View#VISIBLE},
+ * {@link View#INVISIBLE}, or {@link View#GONE}.
+ */
+ @NonNull
+ public Builder setVisibility(@IdRes int id, @Visibility int visibility) {
+ throwIfDestroyed();
+ switch (visibility) {
+ case View.VISIBLE:
+ case View.INVISIBLE:
+ case View.GONE:
+ mVisibilities.put(id, visibility);
+ return this;
+ }
+ throw new IllegalArgumentException("Invalid visibility: " + visibility);
+ }
+
+ /**
+ * Creates a new {@link VisibilitySetterAction} instance.
+ */
+ @NonNull
+ public VisibilitySetterAction build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new VisibilitySetterAction(this);
+ }
+
+ private void throwIfDestroyed() {
+ Preconditions.checkState(!mDestroyed, "Already called build()");
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "VisibilitySetterAction: [" + mVisibilities + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeSparseIntArray(mVisibilities);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<VisibilitySetterAction> CREATOR =
+ new Parcelable.Creator<VisibilitySetterAction>() {
+
+ @NonNull
+ @Override
+ public VisibilitySetterAction 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
+ final SparseIntArray visibilities = parcel.readSparseIntArray();
+ Builder builder = null;
+ for (int i = 0; i < visibilities.size(); i++) {
+ final int id = visibilities.keyAt(i);
+ final int visibility = visibilities.valueAt(i);
+ if (builder == null) {
+ builder = new Builder(id, visibility);
+ } else {
+ builder.setVisibility(id, visibility);
+ }
+ }
+ return builder == null ? null : builder.build();
+ }
+
+ @NonNull
+ @Override
+ public VisibilitySetterAction[] newArray(int size) {
+ return new VisibilitySetterAction[size];
+ }
+ };
+}
diff --git a/android/service/autofill/augmented/AugmentedAutofillService.java b/android/service/autofill/augmented/AugmentedAutofillService.java
new file mode 100644
index 0000000..cca45f5
--- /dev/null
+++ b/android/service/autofill/augmented/AugmentedAutofillService.java
@@ -0,0 +1,677 @@
+/*
+ * 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.augmented;
+
+import static android.service.autofill.augmented.Helper.logResponse;
+import static android.util.TimeUtils.formatDuration;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.BaseBundle;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillEventHistory;
+import android.service.autofill.augmented.PresentationParams.SystemPopupPresentationParams;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+import android.view.autofill.IAugmentedAutofillManagerClient;
+import android.view.autofill.IAutofillWindowPresenter;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A service used to augment the Autofill subsystem by potentially providing autofill data when the
+ * "standard" workflow failed (for example, because the standard AutofillService didn't have data).
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public abstract class AugmentedAutofillService extends Service {
+
+ private static final String TAG = AugmentedAutofillService.class.getSimpleName();
+
+ static boolean sDebug = Build.IS_USER ? false : true;
+ static boolean sVerbose = false;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_AUGMENTED_AUTOFILL_SERVICE} permission so
+ * that other applications can not abuse it.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.autofill.augmented.AugmentedAutofillService";
+
+ private Handler mHandler;
+
+ private SparseArray<AutofillProxy> mAutofillProxies;
+
+ private AutofillProxy mAutofillProxyForLastRequest;
+
+ // Used for metrics / debug only
+ private ComponentName mServiceComponentName;
+
+ private final class AugmentedAutofillServiceImpl extends IAugmentedAutofillService.Stub {
+
+ @Override
+ public void onConnected(boolean debug, boolean verbose) {
+ mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnConnected,
+ AugmentedAutofillService.this, debug, verbose));
+ }
+
+ @Override
+ public void onDisconnected() {
+ mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnDisconnected,
+ AugmentedAutofillService.this));
+ }
+
+ @Override
+ public void onFillRequest(int sessionId, IBinder client, int taskId,
+ ComponentName componentName, AutofillId focusedId, AutofillValue focusedValue,
+ long requestTime, @Nullable InlineSuggestionsRequest inlineSuggestionsRequest,
+ IFillCallback callback) {
+ mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnFillRequest,
+ AugmentedAutofillService.this, sessionId, client, taskId, componentName,
+ focusedId, focusedValue, requestTime, inlineSuggestionsRequest, callback));
+ }
+
+ @Override
+ public void onDestroyAllFillWindowsRequest() {
+ mHandler.sendMessage(
+ obtainMessage(AugmentedAutofillService::handleOnDestroyAllFillWindowsRequest,
+ AugmentedAutofillService.this));
+ }
+ };
+
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler(Looper.getMainLooper(), null, true);
+ BaseBundle.setShouldDefuse(true);
+ }
+
+ /** @hide */
+ @Override
+ public final IBinder onBind(Intent intent) {
+ mServiceComponentName = intent.getComponent();
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return new AugmentedAutofillServiceImpl();
+ }
+ Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+ return null;
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnUnbind,
+ AugmentedAutofillService.this));
+ return false;
+ }
+
+ /**
+ * Called when the Android system connects to service.
+ *
+ * <p>You should generally do initialization here rather than in {@link #onCreate}.
+ */
+ public void onConnected() {
+ }
+
+ /**
+ * The child class of the service can call this method to initiate an Autofill flow.
+ *
+ * <p> The request would be respected only if the previous augmented autofill request was
+ * made for the same {@code activityComponent} and {@code autofillId}, and the field is
+ * currently on focus.
+ *
+ * <p> The request would start a new autofill flow. It doesn't guarantee that the
+ * {@link AutofillManager} will proceed with the request.
+ *
+ * @param activityComponent the client component for which the autofill is requested for
+ * @param autofillId the client field id for which the autofill is requested for
+ * @return true if the request makes the {@link AutofillManager} start a new Autofill flow,
+ * false otherwise.
+ */
+ public final boolean requestAutofill(@NonNull ComponentName activityComponent,
+ @NonNull AutofillId autofillId) {
+ // TODO(b/149531989): revisit this. The request should start a new autofill session
+ // rather than reusing the existing session.
+ final AutofillProxy proxy = mAutofillProxyForLastRequest;
+ if (proxy == null || !proxy.mComponentName.equals(activityComponent)
+ || !proxy.mFocusedId.equals(autofillId)) {
+ return false;
+ }
+ try {
+ return proxy.requestAutofill();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return false;
+ }
+
+ /**
+ * Asks the service to handle an "augmented" autofill request.
+ *
+ * <p>This method is called when the "stantard" autofill service cannot handle a request, which
+ * typically occurs when:
+ * <ul>
+ * <li>Service does not recognize what should be autofilled.
+ * <li>Service does not have data to fill the request.
+ * <li>Service blacklisted that app (or activity) for autofill.
+ * <li>App disabled itself for autofill.
+ * </ul>
+ *
+ * <p>Differently from the standard autofill workflow, on augmented autofill the service is
+ * responsible to generate the autofill UI and request the Android system to autofill the
+ * activity when the user taps an action in that UI (through the
+ * {@link FillController#autofill(List)} method).
+ *
+ * <p>The service <b>MUST</b> call {@link
+ * FillCallback#onSuccess(android.service.autofill.augmented.FillResponse)} as soon as possible,
+ * passing {@code null} when it cannot fulfill the request.
+ * @param request the request to handle.
+ * @param cancellationSignal signal for observing cancellation requests. The system will use
+ * this to notify you that the fill result is no longer needed and you should stop
+ * handling this fill request in order to save resources.
+ * @param controller object used to interact with the autofill system.
+ * @param callback object used to notify the result of the request. Service <b>must</b> call
+ * {@link FillCallback#onSuccess(android.service.autofill.augmented.FillResponse)}.
+ */
+ public void onFillRequest(@NonNull FillRequest request,
+ @NonNull CancellationSignal cancellationSignal, @NonNull FillController controller,
+ @NonNull FillCallback callback) {
+ }
+
+ /**
+ * Called when the Android system disconnects from the service.
+ *
+ * <p> At this point this service may no longer be an active {@link AugmentedAutofillService}.
+ * It should not make calls on {@link AutofillManager} that requires the caller to be
+ * the current service.
+ */
+ public void onDisconnected() {
+ }
+
+ private void handleOnConnected(boolean debug, boolean verbose) {
+ if (sDebug || debug) {
+ Log.d(TAG, "handleOnConnected(): debug=" + debug + ", verbose=" + verbose);
+ }
+ sDebug = debug;
+ sVerbose = verbose;
+ onConnected();
+ }
+
+ private void handleOnDisconnected() {
+ onDisconnected();
+ }
+
+ private void handleOnFillRequest(int sessionId, @NonNull IBinder client, int taskId,
+ @NonNull ComponentName componentName, @NonNull AutofillId focusedId,
+ @Nullable AutofillValue focusedValue, long requestTime,
+ @Nullable InlineSuggestionsRequest inlineSuggestionsRequest,
+ @NonNull IFillCallback callback) {
+ if (mAutofillProxies == null) {
+ mAutofillProxies = new SparseArray<>();
+ }
+
+ final ICancellationSignal transport = CancellationSignal.createTransport();
+ final CancellationSignal cancellationSignal = CancellationSignal.fromTransport(transport);
+ AutofillProxy proxy = mAutofillProxies.get(sessionId);
+ if (proxy == null) {
+ proxy = new AutofillProxy(sessionId, client, taskId, mServiceComponentName,
+ componentName, focusedId, focusedValue, requestTime, callback,
+ cancellationSignal);
+ mAutofillProxies.put(sessionId, proxy);
+ } else {
+ // TODO(b/123099468): figure out if it's ok to reuse the proxy; add logging
+ if (sDebug) Log.d(TAG, "Reusing proxy for session " + sessionId);
+ proxy.update(focusedId, focusedValue, callback, cancellationSignal);
+ }
+
+ try {
+ callback.onCancellable(transport);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ mAutofillProxyForLastRequest = proxy;
+ onFillRequest(new FillRequest(proxy, inlineSuggestionsRequest), cancellationSignal,
+ new FillController(proxy), new FillCallback(proxy));
+ }
+
+ private void handleOnDestroyAllFillWindowsRequest() {
+ if (mAutofillProxies != null) {
+ final int size = mAutofillProxies.size();
+ for (int i = 0; i < size; i++) {
+ final int sessionId = mAutofillProxies.keyAt(i);
+ final AutofillProxy proxy = mAutofillProxies.valueAt(i);
+ if (proxy == null) {
+ // TODO(b/123100811): this might be fine, in which case we should logv it
+ Log.w(TAG, "No proxy for session " + sessionId);
+ return;
+ }
+ if (proxy.mCallback != null) {
+ try {
+ if (!proxy.mCallback.isCompleted()) {
+ proxy.mCallback.cancel();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "failed to check current pending request status", e);
+ }
+ }
+ proxy.destroy();
+ }
+ mAutofillProxies.clear();
+ mAutofillProxyForLastRequest = null;
+ }
+ }
+
+ private void handleOnUnbind() {
+ if (mAutofillProxies == null) {
+ if (sDebug) Log.d(TAG, "onUnbind(): no proxy to destroy");
+ return;
+ }
+ final int size = mAutofillProxies.size();
+ if (sDebug) Log.d(TAG, "onUnbind(): destroying " + size + " proxies");
+ for (int i = 0; i < size; i++) {
+ final AutofillProxy proxy = mAutofillProxies.valueAt(i);
+ try {
+ proxy.destroy();
+ } catch (Exception e) {
+ Log.w(TAG, "error destroying " + proxy);
+ }
+ }
+ mAutofillProxies = null;
+ mAutofillProxyForLastRequest = null;
+ }
+
+ @Override
+ /** @hide */
+ protected final void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.print("Service component: "); pw.println(
+ ComponentName.flattenToShortString(mServiceComponentName));
+ if (mAutofillProxies != null) {
+ final int size = mAutofillProxies.size();
+ pw.print("Number proxies: "); pw.println(size);
+ for (int i = 0; i < size; i++) {
+ final int sessionId = mAutofillProxies.keyAt(i);
+ final AutofillProxy proxy = mAutofillProxies.valueAt(i);
+ pw.print(i); pw.print(") SessionId="); pw.print(sessionId); pw.println(":");
+ proxy.dump(" ", pw);
+ }
+ }
+ dump(pw, args);
+ }
+
+ /**
+ * Implementation specific {@code dump}. The child class can override the method to provide
+ * additional information about the Service's state into the dumpsys output.
+ *
+ * @param pw The PrintWriter to which you should dump your state. This will be closed for
+ * you after you return.
+ * @param args additional arguments to the dump request.
+ */
+ protected void dump(@NonNull PrintWriter pw,
+ @SuppressWarnings("unused") @NonNull String[] args) {
+ pw.print(getClass().getName()); pw.println(": nothing to dump");
+ }
+
+ /**
+ * Gets the inline augmented autofill events that happened after the last
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillController, FillCallback)} call.
+ *
+ * <p>The history is not persisted over reboots, and it's cleared every time the service
+ * replies to a
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillController, FillCallback)}
+ * by calling {@link FillCallback#onSuccess(FillResponse)}. Hence, the service should call
+ * {@link #getFillEventHistory() before finishing the {@link FillCallback}.
+ *
+ * <p>Also note that the events from the dropdown suggestion UI is not stored in the history
+ * since the service owns the UI.
+ *
+ * @return The history or {@code null} if there are no events.
+ */
+ @Nullable public final FillEventHistory getFillEventHistory() {
+ final AutofillManager afm = getSystemService(AutofillManager.class);
+
+ if (afm == null) {
+ return null;
+ } else {
+ return afm.getFillEventHistory();
+ }
+ }
+
+ /** @hide */
+ static final class AutofillProxy {
+
+ static final int REPORT_EVENT_NO_RESPONSE = 1;
+ static final int REPORT_EVENT_UI_SHOWN = 2;
+ static final int REPORT_EVENT_UI_DESTROYED = 3;
+ static final int REPORT_EVENT_INLINE_RESPONSE = 4;
+
+ @IntDef(prefix = { "REPORT_EVENT_" }, value = {
+ REPORT_EVENT_NO_RESPONSE,
+ REPORT_EVENT_UI_SHOWN,
+ REPORT_EVENT_UI_DESTROYED,
+ REPORT_EVENT_INLINE_RESPONSE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ReportEvent{}
+
+
+ private final Object mLock = new Object();
+ private final IAugmentedAutofillManagerClient mClient;
+ private final int mSessionId;
+ public final int mTaskId;
+ public final ComponentName mComponentName;
+ // Used for metrics / debug only
+ private String mServicePackageName;
+ @GuardedBy("mLock")
+ private AutofillId mFocusedId;
+ @GuardedBy("mLock")
+ private AutofillValue mFocusedValue;
+ @GuardedBy("mLock")
+ private IFillCallback mCallback;
+
+ /**
+ * Id of the last field that cause the Autofill UI to be shown.
+ *
+ * <p>Used to make sure the SmartSuggestionsParams is updated when a new fields is focused.
+ */
+ @GuardedBy("mLock")
+ private AutofillId mLastShownId;
+
+ // Objects used to log metrics
+ private final long mFirstRequestTime;
+ private long mFirstOnSuccessTime;
+ private long mUiFirstShownTime;
+ private long mUiFirstDestroyedTime;
+
+ @GuardedBy("mLock")
+ private SystemPopupPresentationParams mSmartSuggestion;
+
+ @GuardedBy("mLock")
+ private FillWindow mFillWindow;
+
+ private CancellationSignal mCancellationSignal;
+
+ private AutofillProxy(int sessionId, @NonNull IBinder client, int taskId,
+ @NonNull ComponentName serviceComponentName,
+ @NonNull ComponentName componentName, @NonNull AutofillId focusedId,
+ @Nullable AutofillValue focusedValue, long requestTime,
+ @NonNull IFillCallback callback, @NonNull CancellationSignal cancellationSignal) {
+ mSessionId = sessionId;
+ mClient = IAugmentedAutofillManagerClient.Stub.asInterface(client);
+ mCallback = callback;
+ mTaskId = taskId;
+ mComponentName = componentName;
+ mServicePackageName = serviceComponentName.getPackageName();
+ mFocusedId = focusedId;
+ mFocusedValue = focusedValue;
+ mFirstRequestTime = requestTime;
+ mCancellationSignal = cancellationSignal;
+ // TODO(b/123099468): linkToDeath
+ }
+
+ @NonNull
+ public SystemPopupPresentationParams getSmartSuggestionParams() {
+ synchronized (mLock) {
+ if (mSmartSuggestion != null && mFocusedId.equals(mLastShownId)) {
+ return mSmartSuggestion;
+ }
+ Rect rect;
+ try {
+ rect = mClient.getViewCoordinates(mFocusedId);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Could not get coordinates for " + mFocusedId);
+ return null;
+ }
+ if (rect == null) {
+ if (sDebug) Log.d(TAG, "getViewCoordinates(" + mFocusedId + ") returned null");
+ return null;
+ }
+ mSmartSuggestion = new SystemPopupPresentationParams(this, rect);
+ mLastShownId = mFocusedId;
+ return mSmartSuggestion;
+ }
+ }
+
+ public void autofill(@NonNull List<Pair<AutofillId, AutofillValue>> pairs)
+ throws RemoteException {
+ final int size = pairs.size();
+ final List<AutofillId> ids = new ArrayList<>(size);
+ final List<AutofillValue> values = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ final Pair<AutofillId, AutofillValue> pair = pairs.get(i);
+ ids.add(pair.first);
+ values.add(pair.second);
+ }
+ final boolean hideHighlight = size == 1 && ids.get(0).equals(mFocusedId);
+ mClient.autofill(mSessionId, ids, values, hideHighlight);
+ }
+
+ public void setFillWindow(@NonNull FillWindow fillWindow) {
+ synchronized (mLock) {
+ mFillWindow = fillWindow;
+ }
+ }
+
+ public FillWindow getFillWindow() {
+ synchronized (mLock) {
+ return mFillWindow;
+ }
+ }
+
+ public void requestShowFillUi(int width, int height, Rect anchorBounds,
+ IAutofillWindowPresenter presenter) throws RemoteException {
+ if (mCancellationSignal.isCanceled()) {
+ if (sVerbose) {
+ Log.v(TAG, "requestShowFillUi() not showing because request is cancelled");
+ }
+ return;
+ }
+ mClient.requestShowFillUi(mSessionId, mFocusedId, width, height, anchorBounds,
+ presenter);
+ }
+
+ public void requestHideFillUi() throws RemoteException {
+ mClient.requestHideFillUi(mSessionId, mFocusedId);
+ }
+
+
+ private boolean requestAutofill() throws RemoteException {
+ return mClient.requestAutofill(mSessionId, mFocusedId);
+ }
+
+ private void update(@NonNull AutofillId focusedId, @NonNull AutofillValue focusedValue,
+ @NonNull IFillCallback callback, @NonNull CancellationSignal cancellationSignal) {
+ synchronized (mLock) {
+ mFocusedId = focusedId;
+ mFocusedValue = focusedValue;
+ if (mCallback != null) {
+ try {
+ if (!mCallback.isCompleted()) {
+ mCallback.cancel();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to check current pending request status", e);
+ }
+ Log.d(TAG, "mCallback is updated.");
+ }
+ mCallback = callback;
+ mCancellationSignal = cancellationSignal;
+ }
+ }
+
+ @NonNull
+ public AutofillId getFocusedId() {
+ synchronized (mLock) {
+ return mFocusedId;
+ }
+ }
+
+ @NonNull
+ public AutofillValue getFocusedValue() {
+ synchronized (mLock) {
+ return mFocusedValue;
+ }
+ }
+
+ void reportResult(@Nullable List<Dataset> inlineSuggestionsData,
+ @Nullable Bundle clientState) {
+ try {
+ mCallback.onSuccess(inlineSuggestionsData, clientState);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling back with the inline suggestions data: " + e);
+ }
+ }
+
+ void logEvent(@ReportEvent int event) {
+ if (sVerbose) Log.v(TAG, "returnAndLogResult(): " + event);
+ long duration = -1;
+ int type = MetricsEvent.TYPE_UNKNOWN;
+
+ switch (event) {
+ case REPORT_EVENT_NO_RESPONSE: {
+ type = MetricsEvent.TYPE_SUCCESS;
+ if (mFirstOnSuccessTime == 0) {
+ mFirstOnSuccessTime = SystemClock.elapsedRealtime();
+ duration = mFirstOnSuccessTime - mFirstRequestTime;
+ if (sDebug) {
+ Log.d(TAG, "Service responded nothing in " + formatDuration(duration));
+ }
+ }
+ } break;
+
+ case REPORT_EVENT_INLINE_RESPONSE: {
+ // TODO: Define a constant and log this event
+ // type = MetricsEvent.TYPE_SUCCESS_INLINE;
+ if (mFirstOnSuccessTime == 0) {
+ mFirstOnSuccessTime = SystemClock.elapsedRealtime();
+ duration = mFirstOnSuccessTime - mFirstRequestTime;
+ if (sDebug) {
+ Log.d(TAG, "Service responded nothing in " + formatDuration(duration));
+ }
+ }
+ } break;
+
+ case REPORT_EVENT_UI_SHOWN: {
+ type = MetricsEvent.TYPE_OPEN;
+ if (mUiFirstShownTime == 0) {
+ mUiFirstShownTime = SystemClock.elapsedRealtime();
+ duration = mUiFirstShownTime - mFirstRequestTime;
+ if (sDebug) Log.d(TAG, "UI shown in " + formatDuration(duration));
+ }
+ } break;
+
+ case REPORT_EVENT_UI_DESTROYED: {
+ type = MetricsEvent.TYPE_CLOSE;
+ if (mUiFirstDestroyedTime == 0) {
+ mUiFirstDestroyedTime = SystemClock.elapsedRealtime();
+ duration = mUiFirstDestroyedTime - mFirstRequestTime;
+ if (sDebug) Log.d(TAG, "UI destroyed in " + formatDuration(duration));
+ }
+ } break;
+
+ default:
+ Log.w(TAG, "invalid event reported: " + event);
+ }
+ logResponse(type, mServicePackageName, mComponentName, mSessionId, duration);
+ }
+
+ public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ pw.print(prefix); pw.print("sessionId: "); pw.println(mSessionId);
+ pw.print(prefix); pw.print("taskId: "); pw.println(mTaskId);
+ pw.print(prefix); pw.print("component: ");
+ pw.println(mComponentName.flattenToShortString());
+ pw.print(prefix); pw.print("focusedId: "); pw.println(mFocusedId);
+ if (mFocusedValue != null) {
+ pw.print(prefix); pw.print("focusedValue: "); pw.println(mFocusedValue);
+ }
+ if (mLastShownId != null) {
+ pw.print(prefix); pw.print("lastShownId: "); pw.println(mLastShownId);
+ }
+ pw.print(prefix); pw.print("client: "); pw.println(mClient);
+ final String prefix2 = prefix + " ";
+ if (mFillWindow != null) {
+ pw.print(prefix); pw.println("window:");
+ mFillWindow.dump(prefix2, pw);
+ }
+ if (mSmartSuggestion != null) {
+ pw.print(prefix); pw.println("smartSuggestion:");
+ mSmartSuggestion.dump(prefix2, pw);
+ }
+ if (mFirstOnSuccessTime > 0) {
+ final long responseTime = mFirstOnSuccessTime - mFirstRequestTime;
+ pw.print(prefix); pw.print("response time: ");
+ formatDuration(responseTime, pw); pw.println();
+ }
+
+ if (mUiFirstShownTime > 0) {
+ final long uiRenderingTime = mUiFirstShownTime - mFirstRequestTime;
+ pw.print(prefix); pw.print("UI rendering time: ");
+ formatDuration(uiRenderingTime, pw); pw.println();
+ }
+
+ if (mUiFirstDestroyedTime > 0) {
+ final long uiTotalTime = mUiFirstDestroyedTime - mFirstRequestTime;
+ pw.print(prefix); pw.print("UI life time: ");
+ formatDuration(uiTotalTime, pw); pw.println();
+ }
+ }
+
+ private void destroy() {
+ synchronized (mLock) {
+ if (mFillWindow != null) {
+ if (sDebug) Log.d(TAG, "destroying window");
+ mFillWindow.destroy();
+ mFillWindow = null;
+ }
+ }
+ }
+ }
+}
diff --git a/android/service/autofill/augmented/FillCallback.java b/android/service/autofill/augmented/FillCallback.java
new file mode 100644
index 0000000..21738d8
--- /dev/null
+++ b/android/service/autofill/augmented/FillCallback.java
@@ -0,0 +1,79 @@
+/*
+ * 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.augmented;
+
+import static android.service.autofill.augmented.AugmentedAutofillService.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Bundle;
+import android.service.autofill.Dataset;
+import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Callback used to indicate at {@link FillRequest} has been fulfilled.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class FillCallback {
+
+ private static final String TAG = FillCallback.class.getSimpleName();
+
+ private final AutofillProxy mProxy;
+
+ FillCallback(@NonNull AutofillProxy proxy) {
+ mProxy = proxy;
+ }
+
+ /**
+ * Sets the response associated with the request.
+ *
+ * @param response response associated with the request, or {@code null} if the service
+ * could not provide autofill for the request.
+ */
+ public void onSuccess(@Nullable FillResponse response) {
+ if (sDebug) Log.d(TAG, "onSuccess(): " + response);
+
+ if (response == null) {
+ mProxy.logEvent(AutofillProxy.REPORT_EVENT_NO_RESPONSE);
+ mProxy.reportResult(/* inlineSuggestionsData */ null, /* clientState */ null);
+ return;
+ }
+
+ List<Dataset> inlineSuggestions = response.getInlineSuggestions();
+ Bundle clientState = response.getClientState();
+ if (inlineSuggestions != null && !inlineSuggestions.isEmpty()) {
+ mProxy.logEvent(AutofillProxy.REPORT_EVENT_INLINE_RESPONSE);
+ mProxy.reportResult(inlineSuggestions, clientState);
+ return;
+ }
+
+ final FillWindow fillWindow = response.getFillWindow();
+ if (fillWindow != null) {
+ fillWindow.show();
+ }
+ // TODO(b/123099468): must notify the server so it can update the session state to avoid
+ // showing conflicting UIs (for example, if a new request is made to the main autofill
+ // service and it now wants to show something).
+ }
+}
diff --git a/android/service/autofill/augmented/FillController.java b/android/service/autofill/augmented/FillController.java
new file mode 100644
index 0000000..7d552d6
--- /dev/null
+++ b/android/service/autofill/augmented/FillController.java
@@ -0,0 +1,74 @@
+/*
+ * 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.augmented;
+
+import static android.service.autofill.augmented.AugmentedAutofillService.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.RemoteException;
+import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
+import android.util.Log;
+import android.util.Pair;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * Object used to interact with the autofill system.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class FillController {
+ private static final String TAG = FillController.class.getSimpleName();
+
+ private final AutofillProxy mProxy;
+
+ FillController(@NonNull AutofillProxy proxy) {
+ mProxy = proxy;
+ }
+
+ /**
+ * Fills the activity with the provided values.
+ *
+ * <p>As a side effect, the {@link FillWindow} associated with the {@link FillResponse} will be
+ * automatically {@link FillWindow#destroy() destroyed}.
+ */
+ public void autofill(@NonNull List<Pair<AutofillId, AutofillValue>> values) {
+ Preconditions.checkNotNull(values);
+
+ if (sDebug) {
+ Log.d(TAG, "autofill() with " + values.size() + " values");
+ }
+
+ try {
+ mProxy.autofill(values);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+
+ final FillWindow fillWindow = mProxy.getFillWindow();
+ if (fillWindow != null) {
+ fillWindow.destroy();
+ }
+ }
+}
diff --git a/android/service/autofill/augmented/FillRequest.java b/android/service/autofill/augmented/FillRequest.java
new file mode 100644
index 0000000..6927cf6
--- /dev/null
+++ b/android/service/autofill/augmented/FillRequest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.augmented;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.ComponentName;
+import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * Represents a request to augment-fill an activity.
+ * @hide
+ */
+@SystemApi
+// TODO(b/123100811): pass a requestId and/or sessionId?
+@TestApi
+@DataClass(
+ genToString = true,
+ genBuilder = false,
+ genHiddenConstructor = true)
[email protected]({"getProxy"})
+public final class FillRequest {
+
+ private final @NonNull AutofillProxy mProxy;
+
+ //TODO(b/146901891): add detailed docs once we have stable APIs.
+ /**
+ * An optional request for inline suggestions.
+ */
+ private final @Nullable InlineSuggestionsRequest mInlineSuggestionsRequest;
+
+ /**
+ * Gets the task of the activity associated with this request.
+ */
+ public int getTaskId() {
+ return mProxy.mTaskId;
+ }
+
+ /**
+ * Gets the name of the activity associated with this request.
+ */
+ @NonNull
+ public ComponentName getActivityComponent() {
+ return mProxy.mComponentName;
+ }
+
+ /**
+ * Gets the id of the field that triggered the request.
+ */
+ @NonNull
+ public AutofillId getFocusedId() {
+ return mProxy.getFocusedId();
+ }
+
+ /**
+ * Gets the current value of the field that triggered the request.
+ */
+ @NonNull
+ public AutofillValue getFocusedValue() {
+ return mProxy.getFocusedValue();
+ }
+
+ /**
+ * Gets the Smart Suggestions object used to embed the autofill UI.
+ *
+ * @return object used to embed the autofill UI, or {@code null} if not supported.
+ */
+ @Nullable
+ public PresentationParams getPresentationParams() {
+ return mProxy.getSmartSuggestionParams();
+ }
+
+ String proxyToString() {
+ return "FillRequest[act=" + getActivityComponent().flattenToShortString()
+ + ", id=" + mProxy.getFocusedId() + "]";
+ }
+
+
+
+
+ // Code below generated by codegen v1.0.14.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/augmented/FillRequest.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new FillRequest.
+ *
+ * @param inlineSuggestionsRequest
+ * An optional request for inline suggestions.
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public FillRequest(
+ @NonNull AutofillProxy proxy,
+ @Nullable InlineSuggestionsRequest inlineSuggestionsRequest) {
+ this.mProxy = proxy;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mProxy);
+ this.mInlineSuggestionsRequest = inlineSuggestionsRequest;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * An optional request for inline suggestions.
+ */
+ @DataClass.Generated.Member
+ public @Nullable InlineSuggestionsRequest getInlineSuggestionsRequest() {
+ return mInlineSuggestionsRequest;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "FillRequest { " +
+ "proxy = " + proxyToString() + ", " +
+ "inlineSuggestionsRequest = " + mInlineSuggestionsRequest +
+ " }";
+ }
+
+ @DataClass.Generated(
+ time = 1577399314707L,
+ codegenVersion = "1.0.14",
+ sourceFile = "frameworks/base/core/java/android/service/autofill/augmented/FillRequest.java",
+ inputSignatures = "private final @android.annotation.NonNull android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy mProxy\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\npublic int getTaskId()\npublic @android.annotation.NonNull android.content.ComponentName getActivityComponent()\npublic @android.annotation.NonNull android.view.autofill.AutofillId getFocusedId()\npublic @android.annotation.NonNull android.view.autofill.AutofillValue getFocusedValue()\npublic @android.annotation.Nullable android.service.autofill.augmented.PresentationParams getPresentationParams()\n java.lang.String proxyToString()\nclass FillRequest extends java.lang.Object implements []\[email protected](genToString=true, genBuilder=false, genHiddenConstructor=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/service/autofill/augmented/FillResponse.java b/android/service/autofill/augmented/FillResponse.java
new file mode 100644
index 0000000..f72eb78
--- /dev/null
+++ b/android/service/autofill/augmented/FillResponse.java
@@ -0,0 +1,241 @@
+/*
+ * 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.augmented;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Bundle;
+import android.service.autofill.Dataset;
+
+import com.android.internal.util.DataClass;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Response to a {@link FillRequest}.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+@DataClass(
+ genBuilder = true,
+ genHiddenGetters = true)
+public final class FillResponse {
+
+ /**
+ * The {@link FillWindow} used to display the Autofill UI.
+ */
+ private @Nullable FillWindow mFillWindow;
+
+ /**
+ * The {@link Dataset}s representing the inline suggestions data. Defaults to null if no
+ * inline suggestions are available from the service.
+ */
+ @DataClass.PluralOf("inlineSuggestion")
+ private @Nullable List<Dataset> mInlineSuggestions;
+
+ /**
+ * The client state that {@link AugmentedAutofillService} implementation can put anything in to
+ * identify the request and the response when calling
+ * {@link AugmentedAutofillService#getFillEventHistory()}.
+ */
+ private @Nullable Bundle mClientState;
+
+ private static FillWindow defaultFillWindow() {
+ return null;
+ }
+
+ private static List<Dataset> defaultInlineSuggestions() {
+ return null;
+ }
+
+ private static Bundle defaultClientState() {
+ return null;
+ }
+
+
+ /** @hide */
+ abstract static class BaseBuilder {
+ abstract FillResponse.Builder addInlineSuggestion(@NonNull Dataset value);
+ }
+
+
+
+ // Code below generated by codegen v1.0.15.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/augmented/FillResponse.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ @DataClass.Generated.Member
+ /* package-private */ FillResponse(
+ @Nullable FillWindow fillWindow,
+ @Nullable List<Dataset> inlineSuggestions,
+ @Nullable Bundle clientState) {
+ this.mFillWindow = fillWindow;
+ this.mInlineSuggestions = inlineSuggestions;
+ this.mClientState = clientState;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * The {@link FillWindow} used to display the Autofill UI.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public @Nullable FillWindow getFillWindow() {
+ return mFillWindow;
+ }
+
+ /**
+ * The {@link Dataset}s representing the inline suggestions data. Defaults to null if no
+ * inline suggestions are available from the service.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public @Nullable List<Dataset> getInlineSuggestions() {
+ return mInlineSuggestions;
+ }
+
+ /**
+ * The client state that {@link AugmentedAutofillService} implementation can put anything in to
+ * identify the request and the response when calling
+ * {@link AugmentedAutofillService#getFillEventHistory()}.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public @Nullable Bundle getClientState() {
+ return mClientState;
+ }
+
+ /**
+ * A builder for {@link FillResponse}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @DataClass.Generated.Member
+ public static final class Builder extends BaseBuilder {
+
+ private @Nullable FillWindow mFillWindow;
+ private @Nullable List<Dataset> mInlineSuggestions;
+ private @Nullable Bundle mClientState;
+
+ private long mBuilderFieldsSet = 0L;
+
+ public Builder() {
+ }
+
+ /**
+ * The {@link FillWindow} used to display the Autofill UI.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setFillWindow(@NonNull FillWindow value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x1;
+ mFillWindow = value;
+ return this;
+ }
+
+ /**
+ * The {@link Dataset}s representing the inline suggestions data. Defaults to null if no
+ * inline suggestions are available from the service.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setInlineSuggestions(@NonNull List<Dataset> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mInlineSuggestions = value;
+ return this;
+ }
+
+ /** @see #setInlineSuggestions */
+ @DataClass.Generated.Member
+ @Override
+ @NonNull FillResponse.Builder addInlineSuggestion(@NonNull Dataset value) {
+ if (mInlineSuggestions == null) setInlineSuggestions(new ArrayList<>());
+ mInlineSuggestions.add(value);
+ return this;
+ }
+
+ /**
+ * The client state that {@link AugmentedAutofillService} implementation can put anything in to
+ * identify the request and the response when calling
+ * {@link AugmentedAutofillService#getFillEventHistory()}.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setClientState(@NonNull Bundle value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4;
+ mClientState = value;
+ return this;
+ }
+
+ /** Builds the instance. This builder should not be touched after calling this! */
+ public @NonNull FillResponse build() {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x8; // Mark builder used
+
+ if ((mBuilderFieldsSet & 0x1) == 0) {
+ mFillWindow = defaultFillWindow();
+ }
+ if ((mBuilderFieldsSet & 0x2) == 0) {
+ mInlineSuggestions = defaultInlineSuggestions();
+ }
+ if ((mBuilderFieldsSet & 0x4) == 0) {
+ mClientState = defaultClientState();
+ }
+ FillResponse o = new FillResponse(
+ mFillWindow,
+ mInlineSuggestions,
+ mClientState);
+ return o;
+ }
+
+ private void checkNotUsed() {
+ if ((mBuilderFieldsSet & 0x8) != 0) {
+ throw new IllegalStateException(
+ "This Builder should not be reused. Use a new Builder instance instead");
+ }
+ }
+ }
+
+ @DataClass.Generated(
+ time = 1584480900526L,
+ codegenVersion = "1.0.15",
+ sourceFile = "frameworks/base/core/java/android/service/autofill/augmented/FillResponse.java",
+ inputSignatures = "private @android.annotation.Nullable android.service.autofill.augmented.FillWindow mFillWindow\nprivate @com.android.internal.util.DataClass.PluralOf(\"inlineSuggestion\") @android.annotation.Nullable java.util.List<android.service.autofill.Dataset> mInlineSuggestions\nprivate @android.annotation.Nullable android.os.Bundle mClientState\nprivate static android.service.autofill.augmented.FillWindow defaultFillWindow()\nprivate static java.util.List<android.service.autofill.Dataset> defaultInlineSuggestions()\nprivate static android.os.Bundle defaultClientState()\nclass FillResponse extends java.lang.Object implements []\[email protected](genBuilder=true, genHiddenGetters=true)\nabstract android.service.autofill.augmented.FillResponse.Builder addInlineSuggestion(android.service.autofill.Dataset)\nclass BaseBuilder extends java.lang.Object implements []")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/service/autofill/augmented/FillWindow.java b/android/service/autofill/augmented/FillWindow.java
new file mode 100644
index 0000000..077df6c
--- /dev/null
+++ b/android/service/autofill/augmented/FillWindow.java
@@ -0,0 +1,323 @@
+/*
+ * 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.augmented;
+
+import static android.service.autofill.augmented.AugmentedAutofillService.sDebug;
+import static android.service.autofill.augmented.AugmentedAutofillService.sVerbose;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
+import android.service.autofill.augmented.PresentationParams.Area;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.autofill.IAutofillWindowPresenter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import dalvik.system.CloseGuard;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+
+/**
+ * Handle to a window used to display the augmented autofill UI.
+ *
+ * <p>The steps to create an augmented autofill UI are:
+ *
+ * <ol>
+ * <li>Gets the {@link PresentationParams} from the {@link FillRequest}.
+ * <li>Gets the {@link Area} to display the UI (for example, through
+ * {@link PresentationParams#getSuggestionArea()}.
+ * <li>Creates a {@link View} that must fit in the {@link Area#getBounds() area boundaries}.
+ * <li>Set the proper listeners to the view (for example, a click listener that
+ * triggers {@link FillController#autofill(java.util.List)}
+ * <li>Call {@link #update(Area, View, long)} with these arguments.
+ * <li>Create a {@link FillResponse} with the {@link FillWindow}.
+ * <li>Pass such {@link FillResponse} to {@link FillCallback#onSuccess(FillResponse)}.
+ * </ol>
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class FillWindow implements AutoCloseable {
+ private static final String TAG = FillWindow.class.getSimpleName();
+
+ private final Object mLock = new Object();
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private final @NonNull Handler mUiThreadHandler = new Handler(Looper.getMainLooper());
+
+ @GuardedBy("mLock")
+ private @NonNull WindowManager mWm;
+ @GuardedBy("mLock")
+ private View mFillView;
+ @GuardedBy("mLock")
+ private boolean mShowing;
+ @GuardedBy("mLock")
+ private @Nullable Rect mBounds;
+
+ @GuardedBy("mLock")
+ private boolean mUpdateCalled;
+ @GuardedBy("mLock")
+ private boolean mDestroyed;
+
+ private @NonNull AutofillProxy mProxy;
+
+ /**
+ * Updates the content of the window.
+ *
+ * @param rootView new root view
+ * @param area coordinates to render the view.
+ * @param flags currently not used.
+ *
+ * @return boolean whether the window was updated or not.
+ *
+ * @throws IllegalArgumentException if the area is not compatible with this window
+ */
+ public boolean update(@NonNull Area area, @NonNull View rootView, long flags) {
+ if (sDebug) {
+ Log.d(TAG, "Updating " + area + " + with " + rootView);
+ }
+ // TODO(b/123100712): add test case for null
+ Preconditions.checkNotNull(area);
+ Preconditions.checkNotNull(area.proxy);
+ Preconditions.checkNotNull(rootView);
+ // TODO(b/123100712): must check the area is a valid object returned by
+ // SmartSuggestionParams, throw IAE if not
+
+ final PresentationParams smartSuggestion = area.proxy.getSmartSuggestionParams();
+ if (smartSuggestion == null) {
+ Log.w(TAG, "No SmartSuggestionParams");
+ return false;
+ }
+
+ final Rect rect = area.getBounds();
+ if (rect == null) {
+ Log.wtf(TAG, "No Rect on SmartSuggestionParams");
+ return false;
+ }
+
+ synchronized (mLock) {
+ checkNotDestroyedLocked();
+
+ mProxy = area.proxy;
+
+ // TODO(b/123227534): once we have the SurfaceControl approach, we should update the
+ // window instead of destroying. In fact, it might be better to allocate a full window
+ // initially, which is transparent (and let touches get through) everywhere but in the
+ // rect boundaries.
+
+ // TODO(b/123099468): make sure all touch events are handled, window is always closed,
+ // etc.
+
+ mWm = rootView.getContext().getSystemService(WindowManager.class);
+ mFillView = rootView;
+ // Listen to the touch outside to destroy the window when typing is detected.
+ mFillView.setOnTouchListener(
+ (view, motionEvent) -> {
+ if (motionEvent.getAction() == MotionEvent.ACTION_OUTSIDE) {
+ if (sVerbose) Log.v(TAG, "Outside touch detected, hiding the window");
+ hide();
+ }
+ return false;
+ }
+ );
+ mShowing = false;
+ mBounds = new Rect(area.getBounds());
+ if (sDebug) {
+ Log.d(TAG, "Created FillWindow: params= " + smartSuggestion + " view=" + rootView);
+ }
+ mUpdateCalled = true;
+ mDestroyed = false;
+ mProxy.setFillWindow(this);
+ return true;
+ }
+ }
+
+ /** @hide */
+ void show() {
+ // TODO(b/123100712): check if updated first / throw exception
+ if (sDebug) Log.d(TAG, "show()");
+ synchronized (mLock) {
+ checkNotDestroyedLocked();
+ if (mWm == null || mFillView == null) {
+ throw new IllegalStateException("update() not called yet, or already destroyed()");
+ }
+ if (mProxy != null) {
+ try {
+ mProxy.requestShowFillUi(mBounds.right - mBounds.left,
+ mBounds.bottom - mBounds.top,
+ /*anchorBounds=*/ null, new FillWindowPresenter(this));
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error requesting to show fill window", e);
+ }
+ mProxy.logEvent(AutofillProxy.REPORT_EVENT_UI_SHOWN);
+ }
+ }
+ }
+
+ /**
+ * Hides the window.
+ *
+ * <p>The window is not destroyed and can be shown again
+ */
+ private void hide() {
+ if (sDebug) Log.d(TAG, "hide()");
+ synchronized (mLock) {
+ checkNotDestroyedLocked();
+ if (mWm == null || mFillView == null) {
+ throw new IllegalStateException("update() not called yet, or already destroyed()");
+ }
+ if (mProxy != null && mShowing) {
+ try {
+ mProxy.requestHideFillUi();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error requesting to hide fill window", e);
+ }
+ }
+ }
+ }
+
+ private void handleShow(WindowManager.LayoutParams p) {
+ if (sDebug) Log.d(TAG, "handleShow()");
+ synchronized (mLock) {
+ if (mWm != null && mFillView != null) {
+ p.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+ if (!mShowing) {
+ mWm.addView(mFillView, p);
+ mShowing = true;
+ } else {
+ mWm.updateViewLayout(mFillView, p);
+ }
+ }
+ }
+ }
+
+ private void handleHide() {
+ if (sDebug) Log.d(TAG, "handleHide()");
+ synchronized (mLock) {
+ if (mWm != null && mFillView != null && mShowing) {
+ mWm.removeView(mFillView);
+ mShowing = false;
+ }
+ }
+ }
+
+ /**
+ * Destroys the window.
+ *
+ * <p>Once destroyed, this window cannot be used anymore
+ */
+ public void destroy() {
+ if (sDebug) {
+ Log.d(TAG,
+ "destroy(): mDestroyed=" + mDestroyed + " mShowing=" + mShowing + " mFillView="
+ + mFillView);
+ }
+ synchronized (mLock) {
+ if (mDestroyed) return;
+ if (mUpdateCalled) {
+ mFillView.setOnClickListener(null);
+ hide();
+ mProxy.logEvent(AutofillProxy.REPORT_EVENT_UI_DESTROYED);
+ }
+ mDestroyed = true;
+ mCloseGuard.close();
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ mCloseGuard.warnIfOpen();
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void checkNotDestroyedLocked() {
+ if (mDestroyed) {
+ throw new IllegalStateException("already destroyed()");
+ }
+ }
+
+ /** @hide */
+ public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ synchronized (this) {
+ pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed);
+ pw.print(prefix); pw.print("updateCalled: "); pw.println(mUpdateCalled);
+ if (mFillView != null) {
+ pw.print(prefix); pw.print("fill window: ");
+ pw.println(mShowing ? "shown" : "hidden");
+ pw.print(prefix); pw.print("fill view: ");
+ pw.println(mFillView);
+ pw.print(prefix); pw.print("mBounds: ");
+ pw.println(mBounds);
+ pw.print(prefix); pw.print("mWm: ");
+ pw.println(mWm);
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void close() {
+ destroy();
+ }
+
+ private static final class FillWindowPresenter extends IAutofillWindowPresenter.Stub {
+ private final @NonNull WeakReference<FillWindow> mFillWindowReference;
+
+ FillWindowPresenter(@NonNull FillWindow fillWindow) {
+ mFillWindowReference = new WeakReference<>(fillWindow);
+ }
+
+ @Override
+ public void show(WindowManager.LayoutParams p, Rect transitionEpicenter,
+ boolean fitsSystemWindows, int layoutDirection) {
+ if (sDebug) Log.d(TAG, "FillWindowPresenter.show()");
+ final FillWindow fillWindow = mFillWindowReference.get();
+ if (fillWindow != null) {
+ fillWindow.mUiThreadHandler.sendMessage(
+ obtainMessage(FillWindow::handleShow, fillWindow, p));
+ }
+ }
+
+ @Override
+ public void hide(Rect transitionEpicenter) {
+ if (sDebug) Log.d(TAG, "FillWindowPresenter.hide()");
+ final FillWindow fillWindow = mFillWindowReference.get();
+ if (fillWindow != null) {
+ fillWindow.mUiThreadHandler.sendMessage(
+ obtainMessage(FillWindow::handleHide, fillWindow));
+ }
+ }
+ }
+}
diff --git a/android/service/autofill/augmented/Helper.java b/android/service/autofill/augmented/Helper.java
new file mode 100644
index 0000000..afcd8b7
--- /dev/null
+++ b/android/service/autofill/augmented/Helper.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 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.augmented;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.metrics.LogMaker;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+/** @hide */
+public final class Helper {
+
+ private static final MetricsLogger sMetricsLogger = new MetricsLogger();
+
+ /**
+ * Logs a {@code MetricsEvent.AUTOFILL_AUGMENTED_RESPONSE} event.
+ */
+ public static void logResponse(int type, @NonNull String servicePackageName,
+ @NonNull ComponentName componentName, int mSessionId, long durationMs) {
+ final LogMaker log = new LogMaker(MetricsEvent.AUTOFILL_AUGMENTED_RESPONSE)
+ .setType(type)
+ .setComponentName(componentName)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SESSION_ID, mSessionId)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_DURATION, durationMs);
+ sMetricsLogger.write(log);
+ }
+
+ private Helper() {
+ throw new UnsupportedOperationException("contains only static methods");
+ }
+}
diff --git a/android/service/autofill/augmented/PresentationParams.java b/android/service/autofill/augmented/PresentationParams.java
new file mode 100644
index 0000000..8b3a001
--- /dev/null
+++ b/android/service/autofill/augmented/PresentationParams.java
@@ -0,0 +1,116 @@
+/*
+ * 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.augmented;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.graphics.Rect;
+import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
+import android.view.View;
+
+import java.io.PrintWriter;
+
+/**
+ * Abstraction of a "Smart Suggestion" component responsible to embed the autofill UI provided by
+ * the augmented autofill service.
+ *
+ * <p>The Smart Suggestion is represented by a {@link Area} object that contains the
+ * dimensions the smart suggestion window, so the service can use it to calculate the size of the
+ * view that will be passed to {@link FillWindow#update(Area, View, long)}.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public abstract class PresentationParams {
+
+ // /** @hide */
+ PresentationParams() {}
+
+ /**
+ * Gets the area of the suggestion strip for the given {@code metadata}
+ *
+ * @return strip dimensions, or {@code null} if the Smart Suggestion provider does not support
+ * suggestions strip.
+ */
+ @Nullable
+ public Area getSuggestionArea() {
+ return null;
+ }
+
+ abstract void dump(String prefix, PrintWriter pw);
+
+ /**
+ * Area associated with a {@link PresentationParams Smart Suggestions} provider.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public abstract static class Area {
+
+ /** @hide */
+ public final AutofillProxy proxy;
+
+ private final Rect mBounds;
+
+ private Area(@NonNull AutofillProxy proxy, @NonNull Rect bounds) {
+ this.proxy = proxy;
+ mBounds = bounds;
+ }
+
+ /**
+ * Gets the area boundaries.
+ */
+ @NonNull
+ public Rect getBounds() {
+ return mBounds;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return mBounds.toString();
+ }
+ }
+
+ /**
+ * System-provided poup window anchored to a view.
+ *
+ * <p>Used just for debugging purposes.
+ *
+ * @hide
+ */
+ public static final class SystemPopupPresentationParams extends PresentationParams {
+ private final Area mSuggestionArea;
+
+ public SystemPopupPresentationParams(@NonNull AutofillProxy proxy, @NonNull Rect rect) {
+ mSuggestionArea = new Area(proxy, rect) {};
+ }
+
+ @Override
+ public Area getSuggestionArea() {
+ return mSuggestionArea;
+ }
+
+ @Override
+ void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ pw.print(prefix); pw.print("area: "); pw.println(mSuggestionArea);
+ }
+ }
+}