Fixed DuplicateIdActivityTest.testDoNotRestoreDuplicateAutofillIds()

It was not consuming the 3rd fill request, which could fail the test due to
race conditions.

Bug: 77271640
Test:  cts-tradefed run commandAndExit cts-dev -m CtsAutoFillServiceTestCases \
       -t DuplicateIdActivityTest#testDoNotRestoreDuplicateAutofillIds

Change-Id: I22f74daaf8d07c7c2804eb5a81c972c9dee0a2f7
Merged-In: I5f8af54b2f3824250b4b1da4e68dd167f8db2b6b

(cherry-picked from commit 1acb8aafc5427e625c9e4868f0f67da648495617)

Change-Id: I896a7dd12993c67331f6c98b46e8d99f5dfeb793
diff --git a/tests/autofillservice/res/layout/duplicate_id_layout.xml b/tests/autofillservice/res/layout/duplicate_id_layout.xml
index a5643ea..4d409a2 100644
--- a/tests/autofillservice/res/layout/duplicate_id_layout.xml
+++ b/tests/autofillservice/res/layout/duplicate_id_layout.xml
@@ -17,17 +17,21 @@
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
     <CheckBox
         android:id="@+id/duplicate_id"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
+        android:layout_height="wrap_content"
+        android:text="cb1" /> <!--  text is set just for debugging purposes -->
 
     <CheckBox
         android:id="@+id/duplicate_id"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
+        android:layout_height="wrap_content"
+        android:text="cb2"/> <!--  text is set just for debugging purposes -->
 
 </LinearLayout>
\ No newline at end of file
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivity.java b/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivity.java
index 31ac8f7..90871ca 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivity.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivity.java
@@ -20,7 +20,7 @@
 import android.util.Log;
 
 public class DuplicateIdActivity extends AbstractAutoFillActivity {
-    private static final String LOG_TAG = DuplicateIdActivity.class.getSimpleName();
+    private static final String TAG = "DuplicateIdActivity";
 
     static final String DUPLICATE_ID = "duplicate_id";
 
@@ -28,9 +28,7 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        if (savedInstanceState != null) {
-            Log.i(LOG_TAG, "onCreate(" + savedInstanceState + ")");
-        }
+        Log.v(TAG, "onCreate(" + savedInstanceState + ")");
 
         setContentView(R.layout.duplicate_id_layout);
     }
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivityTest.java
index 29ec2b1..846dcc4 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivityTest.java
@@ -89,13 +89,14 @@
         // Select field to start autofill
         runShellCommand("input keyevent KEYCODE_TAB");
 
-        InstrumentedAutoFillService.FillRequest request = sReplier.getNextFillRequest();
+        final InstrumentedAutoFillService.FillRequest request1 = sReplier.getNextFillRequest();
+        Log.v(LOG_TAG, "request1: " + request1);
 
-        AssistStructure.ViewNode[] views = findViews(request);
-        AssistStructure.ViewNode view1 = views[0];
-        AssistStructure.ViewNode view2 = views[1];
-        AutofillId id1 = view1.getAutofillId();
-        AutofillId id2 = view2.getAutofillId();
+        final AssistStructure.ViewNode[] views1 = findViews(request1);
+        final AssistStructure.ViewNode view1 = views1[0];
+        final AssistStructure.ViewNode view2 = views1[1];
+        final AutofillId id1 = view1.getAutofillId();
+        final AutofillId id2 = view2.getAutofillId();
 
         Log.i(LOG_TAG, "view1=" + id1);
         Log.i(LOG_TAG, "view2=" + id2);
@@ -106,38 +107,39 @@
         // They got different autofill ids though
         assertThat(id1).isNotEqualTo(id2);
 
+        // Because service returned a null response, rotation will trigger another request.
         sReplier.addResponse(NO_RESPONSE);
-
         // Force rotation to force onDestroy->onCreate cycle
         mUiBot.setScreenOrientation(1);
         // Wait context and Views being recreated in rotation
         mUiBot.assertShownByRelativeId(DUPLICATE_ID);
+        // Ignore 2nd request.
+        final InstrumentedAutoFillService.FillRequest request2 = sReplier.getNextFillRequest();
+        Log.v(LOG_TAG, "request2: " + request2);
 
-        // Because service returned a null response, rotation will trigger another request.
+        // Select other field to trigger new partition (because server didn't return 2nd field
+        // on 1st response)
         sReplier.addResponse(NO_RESPONSE);
-
-        // Select other field to trigger new partition
         runShellCommand("input keyevent KEYCODE_TAB");
 
-        request = sReplier.getNextFillRequest();
-
-        // Ignore 2nd request.
-        sReplier.getNextFillRequest();
-
-        views = findViews(request);
-        AutofillId recreatedId1 = views[0].getAutofillId();
-        AutofillId recreatedId2 = views[1].getAutofillId();
+        final InstrumentedAutoFillService.FillRequest request3 = sReplier.getNextFillRequest();
+        Log.v(LOG_TAG, "request3: " + request3);
+        final AssistStructure.ViewNode[] views2 = findViews(request3);
+        final AssistStructure.ViewNode recreatedView1 = views2[0];
+        final AssistStructure.ViewNode recreatedView2 = views2[1];
+        final AutofillId recreatedId1 = recreatedView1.getAutofillId();
+        final AutofillId recreatedId2 = recreatedView2.getAutofillId();
 
         Log.i(LOG_TAG, "restored view1=" + recreatedId1);
         Log.i(LOG_TAG, "restored view2=" + recreatedId2);
 
         // For the restoring logic the two views are the same. Hence it might happen that the first
-        // view is restored with the id of the second view or the other way round.
+        // view is restored with the autofill id of the second view or the other way round.
         // We just need
         // - to restore as many views as we can (i.e. one)
         // - make sure the autofill ids are still unique after
-        boolean view1WasRestored = (recreatedId1.equals(id1) || recreatedId1.equals(id2));
-        boolean view2WasRestored = (recreatedId2.equals(id1) || recreatedId2.equals(id2));
+        final boolean view1WasRestored = (recreatedId1.equals(id1) || recreatedId1.equals(id2));
+        final boolean view2WasRestored = (recreatedId2.equals(id1) || recreatedId2.equals(id2));
 
         // One id was restored
         assertThat(view1WasRestored || view2WasRestored).isTrue();
diff --git a/tests/autofillservice/src/android/autofillservice/cts/Helper.java b/tests/autofillservice/src/android/autofillservice/cts/Helper.java
index 7934730..4e5418b 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/Helper.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/Helper.java
@@ -156,19 +156,37 @@
         return hasHint(view.getAutofillHints(), id);
     };
 
+    private static String toString(AssistStructure structure, StringBuilder builder) {
+        builder.append("[component=").append(structure.getActivityComponent());
+        final int nodes = structure.getWindowNodeCount();
+        for (int i = 0; i < nodes; i++) {
+            final WindowNode windowNode = structure.getWindowNodeAt(i);
+            dump(builder, windowNode.getRootViewNode(), " ", 0);
+        }
+        return builder.append(']').toString();
+    }
+
+    @NonNull
+    static String toString(@NonNull AssistStructure structure) {
+        return toString(structure, new StringBuilder());
+    }
+
+    @Nullable
+    static String toString(@Nullable AutofillValue value) {
+        if (value == null) return null;
+        if (value.isText()) {
+            // We don't care about PII...
+            final CharSequence text = value.getTextValue();
+            return text == null ? null : text.toString();
+        }
+        return value.toString();
+    }
+
     /**
      * Dump the assist structure on logcat.
      */
     static void dumpStructure(String message, AssistStructure structure) {
-        final StringBuffer buffer = new StringBuffer(message)
-                .append(": component=")
-                .append(structure.getActivityComponent());
-        final int nodes = structure.getWindowNodeCount();
-        for (int i = 0; i < nodes; i++) {
-            final WindowNode windowNode = structure.getWindowNodeAt(i);
-            dump(buffer, windowNode.getRootViewNode(), " ", 0);
-        }
-        Log.i(TAG, buffer.toString());
+        Log.i(TAG, toString(structure, new StringBuilder(message)));
     }
 
     /**
@@ -194,39 +212,69 @@
         SettingsHelper.syncSet(context, USER_SETUP_COMPLETE, complete ? "1" : null);
     }
 
-    private static void dump(StringBuffer buffer, ViewNode node, String prefix, int childId) {
+    private static void dump(@NonNull StringBuilder builder, @NonNull ViewNode node,
+            @NonNull String prefix, int childId) {
         final int childrenSize = node.getChildCount();
-        buffer.append("\n").append(prefix)
-            .append('#').append(childId).append(':')
-            .append("resId=").append(node.getIdEntry())
-            .append(" class=").append(node.getClassName())
-            .append(" text=").append(node.getText())
-            .append(" class=").append(node.getClassName())
-            .append(" webDomain=").append(node.getWebDomain())
-            .append(" #children=").append(childrenSize);
-
-        buffer.append("\n").append(prefix)
-            .append("   afId=").append(node.getAutofillId())
-            .append(" afType=").append(node.getAutofillType())
-            .append(" afValue=").append(node.getAutofillValue())
-            .append(" checked=").append(node.isChecked())
-            .append(" focused=").append(node.isFocused());
-
+        builder.append("\n").append(prefix)
+            .append("child #").append(childId).append(':');
+        append(builder, "afId", node.getAutofillId());
+        append(builder, "afType", node.getAutofillType());
+        append(builder, "afValue", toString(node.getAutofillValue()));
+        append(builder, "resId", node.getIdEntry());
+        append(builder, "class", node.getClassName());
+        append(builder, "text", node.getText());
+        append(builder, "webDomain", node.getWebDomain());
+        append(builder, "checked", node.isChecked());
+        append(builder, "focused", node.isFocused());
         final HtmlInfo htmlInfo = node.getHtmlInfo();
         if (htmlInfo != null) {
-            buffer.append("\nHtmlInfo: tag=").append(htmlInfo.getTag())
-                .append(", attrs: ").append(htmlInfo.getAttributes());
+            builder.append(", HtmlInfo[tag=").append(htmlInfo.getTag())
+                .append(", attrs: ").append(htmlInfo.getAttributes()).append(']');
         }
-
-        prefix += " ";
         if (childrenSize > 0) {
-            for (int i = 0; i < childrenSize; i++) {
-                dump(buffer, node.getChildAt(i), prefix, i);
+            append(builder, "#children", childrenSize).append("\n").append(prefix);
+            prefix += " ";
+            if (childrenSize > 0) {
+                for (int i = 0; i < childrenSize; i++) {
+                    dump(builder, node.getChildAt(i), prefix, i);
+                }
             }
         }
     }
 
     /**
+     * Appends a field value to a {@link StringBuilder} when it's not {@code null}.
+     */
+    @NonNull
+    static StringBuilder append(@NonNull StringBuilder builder, @NonNull String field,
+            @Nullable Object value) {
+        if (value == null) return builder;
+
+        if ((value instanceof Boolean) && ((Boolean) value)) {
+            return builder.append(", ").append(field);
+        }
+
+        if (value instanceof Integer && ((Integer) value) == 0
+                || value instanceof CharSequence && TextUtils.isEmpty((CharSequence) value)) {
+            return builder;
+        }
+
+        return builder.append(", ").append(field).append('=').append(value);
+    }
+
+    /**
+     * Appends a field value to a {@link StringBuilder} when it's {@code true}.
+     */
+    @NonNull
+    static StringBuilder append(@NonNull StringBuilder builder, @NonNull String field,
+            boolean value) {
+        if (value) {
+            builder.append(", ").append(field);
+        }
+        return builder;
+    }
+
+    /**
      * Gets a node if it matches the filter criteria for the given id.
      */
     static ViewNode findNodeByFilter(@NonNull AssistStructure structure, @NonNull Object id,
diff --git a/tests/autofillservice/src/android/autofillservice/cts/InstrumentedAutoFillService.java b/tests/autofillservice/src/android/autofillservice/cts/InstrumentedAutoFillService.java
index 42b6b41..6b47c9f 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/InstrumentedAutoFillService.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/InstrumentedAutoFillService.java
@@ -293,12 +293,13 @@
             this.cancellationSignal = cancellationSignal;
             this.callback = callback;
             this.flags = flags;
-            structure = contexts.get(contexts.size() - 1).getStructure();
+            this.structure = contexts.get(contexts.size() - 1).getStructure();
         }
 
         @Override
         public String toString() {
-            return "FillRequest:" + getActivityName(contexts);
+            return "FillRequest[activity=" + getActivityName(contexts) + ", flags=" + flags
+                    + ", bundle=" + data + ", structure=" + Helper.toString(structure) + "]";
         }
     }