[Autofill PCC]: Allow Providers to set both field and type
Allow providers to set both field and type while creating a dataset.
Also, fix NPE, and issue a request if no detection results available.
Test: atest android.autofillservice.cts.unittests.DatasetTest
BUG: 270423491

Change-Id: I2bcdca5e9f406696389ad712e00d066d1b8cefc5
diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java
index 0ef8bb64..e81ca1a 100644
--- a/core/java/android/service/autofill/Dataset.java
+++ b/core/java/android/service/autofill/Dataset.java
@@ -28,6 +28,7 @@
 import android.content.IntentSender;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.ArrayMap;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillManager;
 import android.view.autofill.AutofillValue;
@@ -434,14 +435,14 @@
      * 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<RemoteViews> mFieldDialogPresentations;
-        private ArrayList<InlinePresentation> mFieldInlinePresentations;
-        private ArrayList<InlinePresentation> mFieldInlineTooltipPresentations;
-        private ArrayList<DatasetFieldFilter> mFieldFilters;
-        private ArrayList<String> mAutofillDatatypes;
+        private ArrayList<AutofillId> mFieldIds = new ArrayList<>();
+        private ArrayList<AutofillValue> mFieldValues = new ArrayList();
+        private ArrayList<RemoteViews> mFieldPresentations = new ArrayList();
+        private ArrayList<RemoteViews> mFieldDialogPresentations = new ArrayList();
+        private ArrayList<InlinePresentation> mFieldInlinePresentations = new ArrayList();
+        private ArrayList<InlinePresentation> mFieldInlineTooltipPresentations = new ArrayList();
+        private ArrayList<DatasetFieldFilter> mFieldFilters = new ArrayList();
+        private ArrayList<String> mAutofillDatatypes = new ArrayList();
         @Nullable private ClipData mFieldContent;
         private RemoteViews mPresentation;
         private RemoteViews mDialogPresentation;
@@ -452,6 +453,15 @@
         @Nullable private String mId;
 
         /**
+         * Usually, a field will be associated with a single autofill id and/or datatype.
+         * There could be null field value corresponding to different autofill ids or datatye
+         * values, but the implementation is ok with duplicating that information.
+         * This map is just for the purpose of optimization, to reduce the size of the pelled data
+         * over the binder transaction.
+         */
+        private ArrayMap<Field, Integer> mFieldToIndexdMap = new ArrayMap<>();
+
+        /**
          * Creates a new builder.
          *
          * @param presentation The presentation used to visualize this dataset.
@@ -1051,29 +1061,40 @@
          */
         public @NonNull Builder setField(@NonNull AutofillId id, @Nullable Field field) {
             throwIfDestroyed();
+
+            if (mFieldToIndexdMap.containsKey(field)) {
+                int index = mFieldToIndexdMap.get(field);
+                if (mFieldIds.get(index) == null) {
+                    mFieldIds.set(index, id);
+                    return this;
+                }
+                // if the Autofill Id is already set, ignore and proceed as if setting in a new
+                // value.
+            }
+            int index;
             if (field == null) {
-                setLifeTheUniverseAndEverything(id, null, null, null, null, null, null);
+                index = setLifeTheUniverseAndEverything(id, null, null, null, null, null, null);
             } else {
                 final DatasetFieldFilter filter = field.getDatasetFieldFilter();
                 final Presentations presentations = field.getPresentations();
                 if (presentations == null) {
-                    setLifeTheUniverseAndEverything(id, field.getValue(), null, null, null,
+                    index = setLifeTheUniverseAndEverything(id, field.getValue(), null, null, null,
                             filter, null);
                 } else {
-                    setLifeTheUniverseAndEverything(id, field.getValue(),
+                    index = setLifeTheUniverseAndEverything(id, field.getValue(),
                             presentations.getMenuPresentation(),
                             presentations.getInlinePresentation(),
                             presentations.getInlineTooltipPresentation(), filter,
                             presentations.getDialogPresentation());
                 }
             }
+            mFieldToIndexdMap.put(field, index);
             return this;
         }
 
         /**
-         * Adds a field to this Dataset with a specific type and no
-         * AutofillId. This is used to send back Field information
-         * when Autofilling with platform detections is on.
+         * Adds a field to this Dataset with a specific type. This is used to send back Field
+         * information when Autofilling with platform detections is on.
          * Platform detections are on when receiving a populated list from
          * FillRequest#getHints().
          *
@@ -1086,9 +1107,6 @@
          * has two credential pairs, then two Datasets should be created,
          * and so on.
          *
-         * Using this will remove any data populated with
-         * setField(@NonNull AutofillId id, @Nullable Field field).
-         *
          * @param hint An autofill hint returned from {@link
          *         FillRequest#getHints()}.
          *
@@ -1102,19 +1120,29 @@
         public @NonNull Dataset.Builder setField(@NonNull String hint, @NonNull Field field) {
             throwIfDestroyed();
 
+            if (mFieldToIndexdMap.containsKey(field)) {
+                int index = mFieldToIndexdMap.get(field);
+                if (mAutofillDatatypes.get(index) == null) {
+                    mAutofillDatatypes.set(index, hint);
+                    return this;
+                }
+                // if the hint is already set, ignore and proceed as if setting in a new hint.
+            }
+
+            int index;
             final DatasetFieldFilter filter = field.getDatasetFieldFilter();
             final Presentations presentations = field.getPresentations();
             if (presentations == null) {
-                setLifeTheUniverseAndEverything(hint, field.getValue(), null, null, null,
+                index = setLifeTheUniverseAndEverything(hint, field.getValue(), null, null, null,
                         filter, null);
             } else {
-                setLifeTheUniverseAndEverything(hint, field.getValue(),
+                index = setLifeTheUniverseAndEverything(hint, field.getValue(),
                         presentations.getMenuPresentation(),
                         presentations.getInlinePresentation(),
                         presentations.getInlineTooltipPresentation(), filter,
                         presentations.getDialogPresentation());
             }
-
+            mFieldToIndexdMap.put(field, index);
             return this;
         }
 
@@ -1172,67 +1200,64 @@
             return this;
         }
 
-        private void setLifeTheUniverseAndEverything(String datatype,
+        /** Returns the index at which this id was modified or inserted */
+        private int setLifeTheUniverseAndEverything(@NonNull String datatype,
                 @Nullable AutofillValue value,
                 @Nullable RemoteViews presentation,
                 @Nullable InlinePresentation inlinePresentation,
                 @Nullable InlinePresentation tooltip,
                 @Nullable DatasetFieldFilter filter,
                 @Nullable RemoteViews dialogPresentation) {
-            if (mAutofillDatatypes == null) {
-                mFieldValues = new ArrayList<>();
-                mFieldPresentations = new ArrayList<>();
-                mFieldDialogPresentations = new ArrayList<>();
-                mFieldInlinePresentations = new ArrayList<>();
-                mFieldInlineTooltipPresentations = new ArrayList<>();
-                mFieldFilters = new ArrayList<>();
-                mAutofillDatatypes = new ArrayList<>();
-                mFieldIds = null;
+            Objects.requireNonNull(datatype, "datatype cannot be null");
+            final int existingIdx = mAutofillDatatypes.indexOf(datatype);
+            if (existingIdx >= 0) {
+                mAutofillDatatypes.add(datatype);
+                mFieldValues.set(existingIdx, value);
+                mFieldPresentations.set(existingIdx, presentation);
+                mFieldDialogPresentations.set(existingIdx, dialogPresentation);
+                mFieldInlinePresentations.set(existingIdx, inlinePresentation);
+                mFieldInlineTooltipPresentations.set(existingIdx, tooltip);
+                mFieldFilters.set(existingIdx, filter);
+                return existingIdx;
             }
+            mFieldIds.add(null);
+            mAutofillDatatypes.add(datatype);
             mFieldValues.add(value);
             mFieldPresentations.add(presentation);
             mFieldDialogPresentations.add(dialogPresentation);
             mFieldInlinePresentations.add(inlinePresentation);
             mFieldInlineTooltipPresentations.add(tooltip);
             mFieldFilters.add(filter);
-            mAutofillDatatypes.add(datatype);
+            return mFieldIds.size() - 1;
         }
 
-        private void setLifeTheUniverseAndEverything(@NonNull AutofillId id,
+        /** Returns the index at which this id was modified or inserted */
+        private int setLifeTheUniverseAndEverything(@NonNull AutofillId id,
                 @Nullable AutofillValue value, @Nullable RemoteViews presentation,
                 @Nullable InlinePresentation inlinePresentation,
                 @Nullable InlinePresentation tooltip,
                 @Nullable DatasetFieldFilter filter,
                 @Nullable RemoteViews dialogPresentation) {
             Objects.requireNonNull(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);
-                    mFieldDialogPresentations.set(existingIdx, dialogPresentation);
-                    mFieldInlinePresentations.set(existingIdx, inlinePresentation);
-                    mFieldInlineTooltipPresentations.set(existingIdx, tooltip);
-                    mFieldFilters.set(existingIdx, filter);
-                    return;
-                }
-            } else {
-                mFieldIds = new ArrayList<>();
-                mFieldValues = new ArrayList<>();
-                mFieldPresentations = new ArrayList<>();
-                mFieldDialogPresentations = new ArrayList<>();
-                mFieldInlinePresentations = new ArrayList<>();
-                mFieldInlineTooltipPresentations = new ArrayList<>();
-                mFieldFilters = new ArrayList<>();
-                mAutofillDatatypes = null;
+            final int existingIdx = mFieldIds.indexOf(id);
+            if (existingIdx >= 0) {
+                mFieldValues.set(existingIdx, value);
+                mFieldPresentations.set(existingIdx, presentation);
+                mFieldDialogPresentations.set(existingIdx, dialogPresentation);
+                mFieldInlinePresentations.set(existingIdx, inlinePresentation);
+                mFieldInlineTooltipPresentations.set(existingIdx, tooltip);
+                mFieldFilters.set(existingIdx, filter);
+                return existingIdx;
             }
             mFieldIds.add(id);
+            mAutofillDatatypes.add(null);
             mFieldValues.add(value);
             mFieldPresentations.add(presentation);
             mFieldDialogPresentations.add(dialogPresentation);
             mFieldInlinePresentations.add(inlinePresentation);
             mFieldInlineTooltipPresentations.add(tooltip);
             mFieldFilters.add(filter);
+            return mFieldIds.size() - 1;
         }
 
         /**
@@ -1249,11 +1274,12 @@
             throwIfDestroyed();
             mDestroyed = true;
             if (mFieldIds == null && mAutofillDatatypes == null) {
-                throw new IllegalStateException("at least one value must be set");
+                throw new IllegalStateException("at least one of field or datatype must be set");
             }
             if (mFieldIds != null && mAutofillDatatypes != null) {
-                if (mFieldIds.size() > 0 && mAutofillDatatypes.size() > 0) {
-                    throw new IllegalStateException("both field and datatype were populated");
+                if (mFieldIds.size() == 0 && mAutofillDatatypes.size() == 0) {
+                    throw new IllegalStateException(
+                            "at least one of field or datatype must be set");
                 }
             }
             if (mFieldContent != null) {
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index b54dbbf..7f6ad43 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -742,6 +742,9 @@
             return Collections.EMPTY_LIST;
         }
         final String typeHints = mService.getMaster().getPccProviderHints();
+        if (sVerbose) {
+            Slog.v(TAG, "TypeHints flag:" + typeHints);
+        }
         if (TextUtils.isEmpty(typeHints)) {
             return new ArrayList<>();
         }
@@ -757,7 +760,7 @@
         @GuardedBy("mLock")
         void maybeRequestFieldClassificationFromServiceLocked() {
             if (mClassificationState.mPendingFieldClassificationRequest == null) {
-                Log.w(TAG, "Received AssistData without pending classification request");
+                Slog.w(TAG, "Received AssistData without pending classification request");
                 return;
             }
 
@@ -791,7 +794,8 @@
             final int requestId = receiverExtras.getInt(EXTRA_REQUEST_ID);
 
             if (sVerbose) {
-                Slog.v(TAG, "New structure for requestId " + requestId + ": " + structure);
+                Slog.v(TAG, "New structure for PCC Detection: requestId " + requestId + ": "
+                        + structure);
             }
 
             synchronized (mLock) {
@@ -1125,6 +1129,13 @@
         // structure is taken. This causes only one fill request per burst of focus changes.
         cancelCurrentRequestLocked();
 
+        if (mClassificationState.mHintsToAutofillIdMap == null) {
+            if (sVerbose) {
+                Slog.v(TAG, "triggering field classification");
+            }
+            requestAssistStructureForPccLocked(flags | FLAG_PCC_DETECTION);
+        }
+
         // Only ask IME to create inline suggestions request when
         // 1. Autofill provider supports it or client enabled client suggestions.
         // 2. The render service is available.
@@ -1376,7 +1387,6 @@
     @Override
     public void onFillRequestSuccess(int requestId, @Nullable FillResponse response,
             @NonNull String servicePackageName, int requestFlags) {
-
         final AutofillId[] fieldClassificationIds;
 
         final LogMaker requestLog;
@@ -1609,10 +1619,68 @@
         Set<Dataset> eligibleDatasets = new ArraySet<>();
         Set<AutofillId> eligibleAutofillIds = new ArraySet<>();
         for (Dataset dataset : response.getDatasets()) {
-            if (dataset.getFieldIds() == null) continue;
+            if (dataset.getFieldIds() == null || dataset.getFieldIds().isEmpty()) continue;
             if (dataset.getAutofillDatatypes() != null
-                    && dataset.getAutofillDatatypes().size() > 0) {
-                continue;
+                    && !dataset.getAutofillDatatypes().isEmpty()) {
+                // This dataset has information relevant for detection too, so we should filter
+                // them out. It's possible that some fields are applicable to hints only, as such,
+                // they need to be filtered off.
+                // TODO(b/266379948): Verify the logic and add tests
+                // Update dataset to only have non-null fieldValues
+
+                // Figure out if we need to process results.
+                boolean conversionRequired = false;
+                int newSize = dataset.getFieldIds().size();
+                for (AutofillId id : dataset.getFieldIds()) {
+                    if (id == null) {
+                        conversionRequired = true;
+                        newSize--;
+                    }
+                }
+
+                if (conversionRequired) {
+                    ArrayList<AutofillId> fieldIds = new ArrayList<>(newSize);
+                    ArrayList<AutofillValue> fieldValues = new ArrayList<>(newSize);
+                    ArrayList<RemoteViews> fieldPresentations = new ArrayList<>(newSize);
+                    ArrayList<RemoteViews> fieldDialogPresentations = new ArrayList<>(newSize);
+                    ArrayList<InlinePresentation> fieldInlinePresentations =
+                            new ArrayList<>(newSize);
+                    ArrayList<InlinePresentation> fieldInlineTooltipPresentations =
+                            new ArrayList<>(newSize);
+                    ArrayList<Dataset.DatasetFieldFilter> fieldFilters = new ArrayList<>(newSize);
+
+                    for (int i = 0; i < dataset.getFieldIds().size(); i++) {
+                        AutofillId id = dataset.getFieldIds().get(i);
+                        if (id != null) {
+                            // Copy over
+                            fieldIds.add(id);
+                            fieldValues.add(dataset.getFieldValues().get(i));
+                            fieldPresentations.add(dataset.getFieldPresentation(i));
+                            fieldDialogPresentations.add(dataset.getFieldDialogPresentation(i));
+                            fieldInlinePresentations.add(dataset.getFieldInlinePresentation(i));
+                            fieldInlineTooltipPresentations.add(
+                                    dataset.getFieldInlineTooltipPresentation(i));
+                            fieldFilters.add(dataset.getFilter(i));
+                        }
+                    }
+                    dataset =
+                            new Dataset(
+                                    fieldIds,
+                                    fieldValues,
+                                    fieldPresentations,
+                                    fieldDialogPresentations,
+                                    fieldInlinePresentations,
+                                    fieldInlineTooltipPresentations,
+                                    fieldFilters,
+                                    new ArrayList<>(),
+                                    dataset.getFieldContent(),
+                                    null,
+                                    null,
+                                    null,
+                                    null,
+                                    dataset.getId(),
+                                    dataset.getAuthentication());
+                }
             }
             eligibleDatasets.add(dataset);
             for (AutofillId id : dataset.getFieldIds()) {
@@ -1639,6 +1707,7 @@
             ArrayMap<String, Set<AutofillId>> hintsToAutofillIdMap =
                     mClassificationState.mHintsToAutofillIdMap;
 
+            // TODO(266379948): Handle group hints too.
             ArrayMap<String, Set<AutofillId>> groupHintsToAutofillIdMap =
                     mClassificationState.mGroupHintsToAutofillIdMap;
 
@@ -1649,7 +1718,8 @@
 
             for (int i = 0; i < datasets.size(); i++) {
                 Dataset dataset = datasets.get(i);
-                if (dataset.getAutofillDatatypes() == null) continue;
+                if (dataset.getAutofillDatatypes() == null
+                        || dataset.getAutofillDatatypes().isEmpty()) continue;
                 if (dataset.getFieldIds() != null && dataset.getFieldIds().size() > 0) continue;
 
                 ArrayList<AutofillId> fieldIds = new ArrayList<>();
@@ -1661,6 +1731,7 @@
                 ArrayList<Dataset.DatasetFieldFilter> fieldFilters = new ArrayList<>();
 
                 for (int j = 0; j < dataset.getAutofillDatatypes().size(); j++) {
+                    if (dataset.getAutofillDatatypes().get(0) == null) continue;
                     String hint = dataset.getAutofillDatatypes().get(j);
 
                     if (hintsToAutofillIdMap.containsKey(hint)) {
@@ -4560,7 +4631,7 @@
         if (mResponses == null) {
             // Set initial capacity as 2 to handle cases where service always requires auth.
             // TODO: add a metric for number of responses set by server, so we can use its average
-            // as the initial array capacitiy.
+            // as the initial array capacity.
             mResponses = new SparseArray<>(2);
         }
         mResponses.put(requestId, newResponse);
@@ -4982,6 +5053,7 @@
             mClassificationGroupHintsMap = new ArrayMap<>();
             mHintsToAutofillIdMap = new ArrayMap<>();
             mGroupHintsToAutofillIdMap = new ArrayMap<>();
+            mClassificationCombinedHintsMap = new ArrayMap<>();
             Set<android.service.assist.classification.FieldClassification> classifications =
                     response.getClassifications();