Basic inline autofill test cases.

Bug: 146453105
Test: atest InlineLoginActivityTest
Change-Id: I7b5772550127f840811e7f39db38a404f9d4c29a
diff --git a/tests/autofillservice/Android.bp b/tests/autofillservice/Android.bp
index d39b86c..7882a76 100644
--- a/tests/autofillservice/Android.bp
+++ b/tests/autofillservice/Android.bp
@@ -17,6 +17,7 @@
     defaults: ["cts_defaults"],
     static_libs: [
         "androidx.annotation_annotation",
+        "androidx.autofill_autofill",
         "androidx.test.ext.junit",
         "compatibility-device-util-axt",
         "ctsdeviceutillegacy-axt",
diff --git a/tests/autofillservice/AndroidManifest.xml b/tests/autofillservice/AndroidManifest.xml
index cf21314..cd52ef5 100644
--- a/tests/autofillservice/AndroidManifest.xml
+++ b/tests/autofillservice/AndroidManifest.xml
@@ -164,6 +164,18 @@
             </meta-data>
         </service>
         <service
+            android:name=".inline.InstrumentedAutoFillServiceInlineEnabled"
+            android:label="InstrumentedAutoFillServiceInlineEnabled"
+            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/autofill_service_inline_enabled">
+            </meta-data>
+        </service>
+        <service
             android:name=".NoOpAutofillService"
             android:label="NoOpAutofillService"
             android:permission="android.permission.BIND_AUTOFILL_SERVICE" >
diff --git a/tests/autofillservice/res/xml/autofill_service_inline_enabled.xml b/tests/autofillservice/res/xml/autofill_service_inline_enabled.xml
new file mode 100644
index 0000000..6e4f2bb
--- /dev/null
+++ b/tests/autofillservice/res/xml/autofill_service_inline_enabled.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+-->
+<autofill-service xmlns:android="http://schemas.android.com/apk/res/android"
+                  android:supportsInlineSuggestions="true">
+</autofill-service>
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AbstractLoginActivityTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/AbstractLoginActivityTestCase.java
index e7c0cf7..e496c24 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/AbstractLoginActivityTestCase.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/AbstractLoginActivityTestCase.java
@@ -48,6 +48,13 @@
     }
 
     /**
+     * Performs a click on username.
+     */
+    protected void requestClickOnUsername() throws TimeoutException {
+        mUiBot.waitForWindowChange(() -> mActivity.onUsername(View::performClick));
+    }
+
+    /**
      * Requests focus on username and expect no Window event happens.
      */
     protected void requestFocusOnUsernameNoWindowChange() {
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AutoFillServiceTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/AutoFillServiceTestCase.java
index a5ac282..54986d6 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/AutoFillServiceTestCase.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/AutoFillServiceTestCase.java
@@ -31,11 +31,16 @@
 import android.content.pm.PackageManager;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
+import android.service.autofill.InlinePresentation;
 import android.util.Log;
+import android.util.Size;
 import android.view.autofill.AutofillManager;
+import android.view.inline.InlinePresentationSpec;
 import android.widget.RemoteViews;
 
 import androidx.annotation.NonNull;
+import androidx.autofill.InlinePresentationBuilder;
+import androidx.test.InstrumentationRegistry;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.compatibility.common.util.DeviceConfigStateChangerRule;
@@ -44,6 +49,7 @@
 import com.android.compatibility.common.util.SafeCleanerRule;
 import com.android.compatibility.common.util.SettingsStateKeeperRule;
 import com.android.compatibility.common.util.TestNameUtils;
+import com.android.cts.mockime.ImeSettings;
 import com.android.cts.mockime.MockImeSessionRule;
 
 import org.junit.AfterClass;
@@ -193,7 +199,10 @@
         };
 
         @ClassRule
-        public static final MockImeSessionRule sMockImeSessionRule = new MockImeSessionRule();
+        public static final MockImeSessionRule sMockImeSessionRule = new MockImeSessionRule(
+                InstrumentationRegistry.getTargetContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder().setInlineSuggestionsEnabled(true));
 
         protected static final RequiredFeatureRule sRequiredFeatureRule =
                 new RequiredFeatureRule(PackageManager.FEATURE_AUTOFILL);
@@ -416,6 +425,12 @@
             return presentation;
         }
 
+        protected InlinePresentation createInlinePresentation(String message) {
+            return new InlinePresentation(new InlinePresentationBuilder(message).build(),
+                    new InlinePresentationSpec.Builder(new Size(100, 100), new Size(400, 100))
+                            .build(), /* pinned= */ false);
+        }
+
         @NonNull
         protected AutofillManager getAutofillManager() {
             return mContext.getSystemService(AutofillManager.class);
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CannedFillResponse.java b/tests/autofillservice/src/android/autofillservice/cts/CannedFillResponse.java
index 5523e7a3..c2094c0 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/CannedFillResponse.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/CannedFillResponse.java
@@ -27,6 +27,7 @@
 import android.service.autofill.FillCallback;
 import android.service.autofill.FillContext;
 import android.service.autofill.FillResponse;
+import android.service.autofill.InlinePresentation;
 import android.service.autofill.SaveInfo;
 import android.service.autofill.UserData;
 import android.util.Log;
@@ -75,6 +76,7 @@
     private final CharSequence mSaveDescription;
     private final Bundle mExtras;
     private final RemoteViews mPresentation;
+    private final InlinePresentation mInlinePresentation;
     private final RemoteViews mHeader;
     private final RemoteViews mFooter;
     private final IntentSender mAuthentication;
@@ -106,6 +108,7 @@
         mSaveType = builder.mSaveType;
         mExtras = builder.mExtras;
         mPresentation = builder.mPresentation;
+        mInlinePresentation = builder.mInlinePresentation;
         mHeader = builder.mHeader;
         mFooter = builder.mFooter;
         mAuthentication = builder.mAuthentication;
@@ -227,7 +230,7 @@
         }
         if (mAuthenticationIds != null) {
             builder.setAuthentication(getAutofillIds(nodeResolver, mAuthenticationIds),
-                    mAuthentication, mPresentation);
+                    mAuthentication, mPresentation, mInlinePresentation);
         }
         if (mDisableDuration > 0) {
             builder.disableAutofill(mDisableDuration);
@@ -278,6 +281,7 @@
                 + ", failureMessage=" + mFailureMessage
                 + ", saveDescription=" + mSaveDescription
                 + ", hasPresentation=" + (mPresentation != null)
+                + ", hasInlinePresentation=" + (mInlinePresentation != null)
                 + ", hasHeader=" + (mHeader != null)
                 + ", hasFooter=" + (mFooter != null)
                 + ", hasAuthentication=" + (mAuthentication != null)
@@ -313,6 +317,7 @@
         public int mSaveType = -1;
         private Bundle mExtras;
         private RemoteViews mPresentation;
+        private InlinePresentation mInlinePresentation;
         private RemoteViews mFooter;
         private RemoteViews mHeader;
         private IntentSender mAuthentication;
@@ -400,6 +405,14 @@
         }
 
         /**
+         * Sets the view to present the response in the UI.
+         */
+        public Builder setInlinePresentation(InlinePresentation inlinePresentation) {
+            mInlinePresentation = inlinePresentation;
+            return this;
+        }
+
+        /**
          * Sets the authentication intent.
          */
         public Builder setAuthentication(IntentSender authentication, String... ids) {
@@ -558,16 +571,20 @@
     public static class CannedDataset {
         private final Map<String, AutofillValue> mFieldValues;
         private final Map<String, RemoteViews> mFieldPresentations;
+        private final Map<String, InlinePresentation> mFieldInlinePresentations;
         private final Map<String, Pair<Boolean, Pattern>> mFieldFilters;
         private final RemoteViews mPresentation;
+        private final InlinePresentation mInlinePresentation;
         private final IntentSender mAuthentication;
         private final String mId;
 
         private CannedDataset(Builder builder) {
             mFieldValues = builder.mFieldValues;
             mFieldPresentations = builder.mFieldPresentations;
+            mFieldInlinePresentations = builder.mFieldInlinePresentations;
             mFieldFilters = builder.mFieldFilters;
             mPresentation = builder.mPresentation;
+            mInlinePresentation = builder.mInlinePresentation;
             mAuthentication = builder.mAuthentication;
             mId = builder.mId;
         }
@@ -576,9 +593,13 @@
          * Creates a new dataset, replacing the field ids by the real ids from the assist structure.
          */
         Dataset asDataset(Function<String, ViewNode> nodeResolver) {
-            final Dataset.Builder builder = (mPresentation == null)
-                    ? new Dataset.Builder()
-                    : new Dataset.Builder(mPresentation);
+            final Dataset.Builder builder = mPresentation != null
+                    ? mInlinePresentation == null
+                    ? new Dataset.Builder(mPresentation)
+                    : new Dataset.Builder(mPresentation, mInlinePresentation)
+                    : mInlinePresentation == null
+                            ? new Dataset.Builder()
+                            : new Dataset.Builder(mInlinePresentation);
 
             if (mFieldValues != null) {
                 for (Map.Entry<String, AutofillValue> entry : mFieldValues.entrySet()) {
@@ -590,18 +611,34 @@
                     final AutofillId autofillId = node.getAutofillId();
                     final AutofillValue value = entry.getValue();
                     final RemoteViews presentation = mFieldPresentations.get(id);
+                    final InlinePresentation inlinePresentation = mFieldInlinePresentations.get(id);
                     final Pair<Boolean, Pattern> filter = mFieldFilters.get(id);
                     if (presentation != null) {
                         if (filter == null) {
-                            builder.setValue(autofillId, value, presentation);
+                            if (inlinePresentation != null) {
+                                builder.setValue(autofillId, value, presentation,
+                                        inlinePresentation);
+                            } else {
+                                builder.setValue(autofillId, value, presentation);
+                            }
                         } else {
-                            builder.setValue(autofillId, value, filter.second, presentation);
+                            if (inlinePresentation != null) {
+                                builder.setValue(autofillId, value, filter.second, presentation,
+                                        inlinePresentation);
+                            } else {
+                                builder.setValue(autofillId, value, filter.second, presentation);
+                            }
                         }
                     } else {
-                        if (filter == null) {
-                            builder.setValue(autofillId, value);
+                        if (inlinePresentation != null) {
+                            builder.setInlinePresentation(autofillId, value,
+                                    filter != null ? filter.second : null, inlinePresentation);
                         } else {
-                            builder.setValue(autofillId, value, filter.second);
+                            if (filter == null) {
+                                builder.setValue(autofillId, value);
+                            } else {
+                                builder.setValue(autofillId, value, filter.second);
+                            }
                         }
                     }
                 }
@@ -613,7 +650,9 @@
         @Override
         public String toString() {
             return "CannedDataset " + mId + " : [hasPresentation=" + (mPresentation != null)
+                    + ", hasInlinePresentation=" + (mInlinePresentation != null)
                     + ", fieldPresentations=" + (mFieldPresentations)
+                    + ", fieldInlinePresentations=" + (mFieldInlinePresentations)
                     + ", hasAuthentication=" + (mAuthentication != null)
                     + ", fieldValues=" + mFieldValues
                     + ", fieldFilters=" + mFieldFilters + "]";
@@ -622,9 +661,12 @@
         public static class Builder {
             private final Map<String, AutofillValue> mFieldValues = new HashMap<>();
             private final Map<String, RemoteViews> mFieldPresentations = new HashMap<>();
+            private final Map<String, InlinePresentation> mFieldInlinePresentations =
+                    new HashMap<>();
             private final Map<String, Pair<Boolean, Pattern>> mFieldFilters = new HashMap<>();
 
             private RemoteViews mPresentation;
+            private InlinePresentation mInlinePresentation;
             private IntentSender mAuthentication;
             private String mId;
 
@@ -749,6 +791,35 @@
             }
 
             /**
+             * Sets the canned value of a field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, String text, RemoteViews presentation,
+                    InlinePresentation inlinePresentation) {
+                setField(id, text);
+                mFieldPresentations.put(id, presentation);
+                mFieldInlinePresentations.put(id, inlinePresentation);
+                return this;
+            }
+
+            /**
+             * Sets the canned value of a field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, String text, RemoteViews presentation,
+                    InlinePresentation inlinePresentation, Pattern filter) {
+                setField(id, text, presentation, inlinePresentation);
+                mFieldFilters.put(id, new Pair<>(true, filter));
+                return this;
+            }
+
+            /**
              * Sets the view to present the response in the UI.
              */
             public Builder setPresentation(RemoteViews presentation) {
@@ -756,6 +827,16 @@
                 return this;
             }
 
+
+
+            /**
+             * Sets the view to present the response in the UI.
+             */
+            public Builder setInlinePresentation(InlinePresentation inlinePresentation) {
+                mInlinePresentation = inlinePresentation;
+                return this;
+            }
+
             /**
              * Sets the authentication intent.
              */
diff --git a/tests/autofillservice/src/android/autofillservice/cts/Timeouts.java b/tests/autofillservice/src/android/autofillservice/cts/Timeouts.java
index d64aa41..a4799c8 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/Timeouts.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/Timeouts.java
@@ -26,7 +26,7 @@
     private static final long ONE_TIMEOUT_TO_RULE_THEN_ALL_MS = 20_000;
     private static final long ONE_NAPTIME_TO_RULE_THEN_ALL_MS = 2_000;
 
-    static final long MOCK_IME_TIMEOUT_MS = 5_000;
+    public static final long MOCK_IME_TIMEOUT_MS = 5_000;
 
     /**
      * Timeout until framework binds / unbinds from service.
diff --git a/tests/autofillservice/src/android/autofillservice/cts/UiBot.java b/tests/autofillservice/src/android/autofillservice/cts/UiBot.java
index 9287e71..15386b0 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/UiBot.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/UiBot.java
@@ -94,6 +94,8 @@
     private static final String RESOURCE_ID_SAVE_BUTTON_NO = "autofill_save_no";
     private static final String RESOURCE_ID_SAVE_BUTTON_YES = "autofill_save_yes";
     private static final String RESOURCE_ID_OVERFLOW = "overflow";
+    //TODO: Change magic constant
+    private static final String RESOURCE_ID_SUGGESTION_STRIP = "message";
 
     private static final String RESOURCE_STRING_SAVE_TITLE = "autofill_save_title";
     private static final String RESOURCE_STRING_SAVE_TITLE_WITH_TYPE =
@@ -132,6 +134,8 @@
     private static final BySelector SAVE_UI_SELECTOR = By.res("android", RESOURCE_ID_SAVE_SNACKBAR);
     private static final BySelector DATASET_HEADER_SELECTOR =
             By.res("android", RESOURCE_ID_DATASET_HEADER);
+    private static final BySelector SUGGESTION_STRIP_SELECTOR =
+            By.res("android", RESOURCE_ID_SUGGESTION_STRIP);
 
     // TODO: figure out a more reliable solution that does not depend on SystemUI resources.
     private static final String SPLIT_WINDOW_DIVIDER_ID =
@@ -331,6 +335,19 @@
         return picker;
     }
 
+    public UiObject2 assertSuggestionStrip(int childrenCount) throws Exception {
+        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
+        assertThat(strip.getChildCount()).isEqualTo(childrenCount);
+        return strip;
+    }
+
+    public void selectSuggestion(int index) throws Exception {
+        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
+        assertThat(index).isAtLeast(0);
+        assertThat(index).isLessThan(strip.getChildCount());
+        strip.getChildren().get(index).click();
+    }
+
     /**
      * Gets the text of this object children.
      */
@@ -1037,6 +1054,10 @@
         return picker;
     }
 
+    private UiObject2 findSuggestionStrip(Timeout timeout) throws Exception {
+        return waitForObject(SUGGESTION_STRIP_SELECTOR, timeout);
+    }
+
     /**
      * Asserts a given object has the expected accessibility title.
      */
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineLoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineLoginActivityTest.java
new file mode 100644
index 0000000..284d5492
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineLoginActivityTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.autofillservice.cts.inline;
+
+import static android.autofillservice.cts.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.Helper.ID_USERNAME;
+import static android.autofillservice.cts.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
+import static android.autofillservice.cts.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.Helper.getContext;
+import static android.autofillservice.cts.Timeouts.MOCK_IME_TIMEOUT_MS;
+import static android.autofillservice.cts.inline.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
+
+import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.autofillservice.cts.AbstractLoginActivityTestCase;
+import android.autofillservice.cts.CannedFillResponse;
+import android.autofillservice.cts.Helper;
+import android.autofillservice.cts.InstrumentedAutoFillService;
+import android.os.Process;
+import android.service.autofill.FillContext;
+import android.support.test.uiautomator.UiObject2;
+
+import com.android.compatibility.common.util.RetryableException;
+import com.android.cts.mockime.ImeEventStream;
+import com.android.cts.mockime.MockImeSession;
+
+import org.junit.Test;
+
+import java.util.concurrent.TimeoutException;
+
+public class InlineLoginActivityTest extends AbstractLoginActivityTestCase {
+
+    private static final String TAG = "InlineLoginActivityTest";
+
+    @Override
+    protected void enableService() {
+        Helper.enableAutofillService(getContext(), SERVICE_NAME);
+    }
+
+    @Test
+    public void testAutofill_oneDataset() throws Exception {
+        // Set service.
+        enableService();
+
+        final MockImeSession mockImeSession = sMockImeSessionRule.getMockImeSession();
+        assumeTrue("MockIME not available", mockImeSession != null);
+
+        // Set expectations.
+        String expectedHeader = null, expectedFooter = null;
+
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .setInlinePresentation(createInlinePresentation("The Dude"))
+                        .build());
+
+        sReplier.addResponse(builder.build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Dynamically set password to make sure it's sanitized.
+        mActivity.onPassword((v) -> v.setText("I AM GROOT"));
+
+        final ImeEventStream stream = mockImeSession.openEventStream();
+        mockImeSession.callRequestShowSelf(0);
+
+        // Wait until the MockIme gets bound to the TestActivity.
+        expectBindInput(stream, Process.myPid(), MOCK_IME_TIMEOUT_MS);
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        expectEvent(stream, editorMatcher("onStartInput", mActivity.getUsername().getId()),
+                MOCK_IME_TIMEOUT_MS);
+
+        //TODO: extServices bug cause test to fail first time, retry if suggestion strip missing.
+        try {
+            expectEvent(stream, event -> "onSuggestionViewUpdated".equals(event.getEventName()),
+                    MOCK_IME_TIMEOUT_MS);
+        } catch (TimeoutException e) {
+            sReplier.getNextFillRequest();
+            throw new RetryableException("Retry inline test");
+        }
+
+        final UiObject2 suggestionStrip = mUiBot.assertSuggestionStrip(1);
+        mUiBot.selectSuggestion(0);
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Sanity checks.
+
+        // Make sure input was sanitized.
+        final InstrumentedAutoFillService.FillRequest request = sReplier.getNextFillRequest();
+        assertWithMessage("CancelationSignal is null").that(request.cancellationSignal).isNotNull();
+        assertTextIsSanitized(request.structure, ID_PASSWORD);
+        final FillContext fillContext = request.contexts.get(request.contexts.size() - 1);
+        assertThat(fillContext.getFocusedId())
+                .isEqualTo(findAutofillIdByResourceId(fillContext, ID_USERNAME));
+
+        // Make sure initial focus was properly set.
+        assertWithMessage("Username node is not focused").that(
+                findNodeByResourceId(request.structure, ID_USERNAME).isFocused()).isTrue();
+        assertWithMessage("Password node is focused").that(
+                findNodeByResourceId(request.structure, ID_PASSWORD).isFocused()).isFalse();
+    }
+
+    @Test
+    public void testAutofill_twoDatasets() throws Exception {
+        // Set service.
+        enableService();
+
+        final MockImeSession mockImeSession = sMockImeSessionRule.getMockImeSession();
+        assumeTrue("MockIME not available", mockImeSession != null);
+
+        // Set expectations.
+        String expectedHeader = null, expectedFooter = null;
+
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .setInlinePresentation(createInlinePresentation("The Dude"))
+                        .build())
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "test")
+                        .setField(ID_PASSWORD, "tweet")
+                        .setPresentation(createPresentation("Second Dude"))
+                        .setInlinePresentation(createInlinePresentation("Second Dude"))
+                        .build());
+
+        sReplier.addResponse(builder.build());
+        mActivity.expectAutoFill("test", "tweet");
+
+        // Dynamically set password to make sure it's sanitized.
+        mActivity.onPassword((v) -> v.setText("I AM GROOT"));
+
+        final ImeEventStream stream = mockImeSession.openEventStream();
+        mockImeSession.callRequestShowSelf(0);
+
+        // Wait until the MockIme gets bound to the TestActivity.
+        expectBindInput(stream, Process.myPid(), MOCK_IME_TIMEOUT_MS);
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        expectEvent(stream, editorMatcher("onStartInput", mActivity.getUsername().getId()),
+                MOCK_IME_TIMEOUT_MS);
+
+        //TODO: extServices bug cause test to fail first time, retry if suggestion strip missing.
+        try {
+            expectEvent(stream, event -> "onSuggestionViewUpdated".equals(event.getEventName()),
+                    MOCK_IME_TIMEOUT_MS);
+        } catch (TimeoutException e) {
+            sReplier.getNextFillRequest();
+            throw new RetryableException("Retry inline test");
+        }
+
+        mUiBot.assertSuggestionStrip(2);
+        mUiBot.selectSuggestion(1);
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Sanity checks.
+
+        // Make sure input was sanitized.
+        final InstrumentedAutoFillService.FillRequest request = sReplier.getNextFillRequest();
+        assertWithMessage("CancelationSignal is null").that(request.cancellationSignal).isNotNull();
+        assertTextIsSanitized(request.structure, ID_PASSWORD);
+        final FillContext fillContext = request.contexts.get(request.contexts.size() - 1);
+        assertThat(fillContext.getFocusedId())
+                .isEqualTo(findAutofillIdByResourceId(fillContext, ID_USERNAME));
+
+        // Make sure initial focus was properly set.
+        assertWithMessage("Username node is not focused").that(
+                findNodeByResourceId(request.structure, ID_USERNAME).isFocused()).isTrue();
+        assertWithMessage("Password node is focused").that(
+                findNodeByResourceId(request.structure, ID_PASSWORD).isFocused()).isFalse();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InstrumentedAutoFillServiceInlineEnabled.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InstrumentedAutoFillServiceInlineEnabled.java
new file mode 100644
index 0000000..a931c53
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InstrumentedAutoFillServiceInlineEnabled.java
@@ -0,0 +1,37 @@
+/*
+ * 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.autofillservice.cts.inline;
+
+import android.autofillservice.cts.InstrumentedAutoFillService;
+import android.service.autofill.AutofillService;
+
+/**
+ * Implementation of {@link AutofillService} that has inline suggestions support enabled.
+ */
+public class InstrumentedAutoFillServiceInlineEnabled extends InstrumentedAutoFillService {
+    @SuppressWarnings("hiding")
+    static final String SERVICE_PACKAGE = "android.autofillservice.cts";
+    @SuppressWarnings("hiding")
+    static final String SERVICE_CLASS = "InstrumentedAutoFillServiceInlineEnabled";
+    @SuppressWarnings("hiding")
+    static final String SERVICE_NAME = SERVICE_PACKAGE + "/.inline." + SERVICE_CLASS;
+
+    public InstrumentedAutoFillServiceInlineEnabled() {
+        sInstance.set(this);
+        sServiceLabel = SERVICE_CLASS;
+    }
+}
diff --git a/tests/inputmethod/mockime/res/xml/method.xml b/tests/inputmethod/mockime/res/xml/method.xml
index 2266fba..5b3cf85 100644
--- a/tests/inputmethod/mockime/res/xml/method.xml
+++ b/tests/inputmethod/mockime/res/xml/method.xml
@@ -15,5 +15,6 @@
   limitations under the License.
 -->
 
-<input-method xmlns:android="http://schemas.android.com/apk/res/android">
+<input-method xmlns:android="http://schemas.android.com/apk/res/android"
+              android:supportsInlineSuggestions="true">
 </input-method>
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeSettings.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeSettings.java
index a7bf781..a9d1414 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeSettings.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeSettings.java
@@ -47,6 +47,7 @@
     private static final String INPUT_VIEW_SYSTEM_UI_VISIBILITY = "InputViewSystemUiVisibility";
     private static final String HARD_KEYBOARD_CONFIGURATION_BEHAVIOR_ALLOWED =
             "HardKeyboardConfigurationBehaviorAllowed";
+    private static final String INLINE_SUGGESTIONS_ENABLED = "InlineSuggestionsEnabled";
 
     @NonNull
     private final PersistableBundle mBundle;
@@ -105,6 +106,10 @@
         return mBundle.getBoolean(HARD_KEYBOARD_CONFIGURATION_BEHAVIOR_ALLOWED, defaultValue);
     }
 
+    public boolean getInlineSuggestionsEnabled() {
+        return mBundle.getBoolean(INLINE_SUGGESTIONS_ENABLED);
+    }
+
     static Bundle serializeToBundle(@NonNull String eventCallbackActionName,
             @Nullable Builder builder) {
         final Bundle result = new Bundle();
@@ -214,5 +219,16 @@
             mBundle.putBoolean(HARD_KEYBOARD_CONFIGURATION_BEHAVIOR_ALLOWED, allowed);
             return this;
         }
+
+        /**
+         * Controls whether inline suggestions are enabled for {@link MockIme}. If enabled, a
+         * suggestion strip will be rendered at the top of the keyboard.
+         *
+         * @param enabled {@code true} when {@link MockIme} is enabled to show inline suggestions.
+         */
+        public Builder setInlineSuggestionsEnabled(boolean enabled) {
+            mBundle.putBoolean(INLINE_SUGGESTIONS_ENABLED, enabled);
+            return this;
+        }
     }
 }
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
index 590ce86..ecbe048 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.mockime;
 
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
 
 import android.content.BroadcastReceiver;
@@ -25,6 +27,7 @@
 import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.inputmethodservice.InputMethodService;
+import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -36,31 +39,44 @@
 import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Size;
 import android.util.TypedValue;
 import android.view.Gravity;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.Window;
 import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.inline.InlinePresentationSpec;
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InlineSuggestion;
+import android.view.inputmethod.InlineSuggestionsRequest;
+import android.view.inputmethod.InlineSuggestionsResponse;
 import android.view.inputmethod.InputBinding;
 import android.view.inputmethod.InputContentInfo;
 import android.view.inputmethod.InputMethod;
 import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
+import android.widget.ScrollView;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.CallSuper;
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BooleanSupplier;
 import java.util.function.Consumer;
@@ -407,23 +423,38 @@
 
             final int defaultBackgroundColor =
                     getResources().getColor(android.R.color.holo_orange_dark, null);
-            setBackgroundColor(mSettings.getBackgroundColor(defaultBackgroundColor));
 
             final int mainSpacerHeight = mSettings.getInputViewHeightWithoutSystemWindowInset(
                     LayoutParams.WRAP_CONTENT);
             {
-                final RelativeLayout layout = new RelativeLayout(getContext());
+                final LinearLayout layout = new LinearLayout(getContext());
+                layout.setOrientation(LinearLayout.VERTICAL);
+
+                if (mSettings.getInlineSuggestionsEnabled()) {
+                    final ScrollView scrollView = new ScrollView(getContext());
+                    final LayoutParams scrollViewParams = new LayoutParams(MATCH_PARENT, 100);
+                    scrollView.setLayoutParams(scrollViewParams);
+
+                    sSuggestionView = new LinearLayout(getContext());
+                    sSuggestionView.setBackgroundColor(0xFFEEEEEE);
+                    //TODO: Change magic id
+                    sSuggestionView.setId(0x0102000b);
+                    scrollView.addView(sSuggestionView,
+                            new LayoutParams(MATCH_PARENT, MATCH_PARENT));
+
+                    layout.addView(scrollView);
+                }
+
                 final TextView textView = new TextView(getContext());
-                final RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
-                        RelativeLayout.LayoutParams.MATCH_PARENT,
-                        RelativeLayout.LayoutParams.WRAP_CONTENT);
-                params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
+                final LayoutParams params = new LayoutParams(MATCH_PARENT, WRAP_CONTENT);
                 textView.setLayoutParams(params);
                 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
                 textView.setGravity(Gravity.CENTER);
                 textView.setText(getImeId());
+                textView.setBackgroundColor(mSettings.getBackgroundColor(defaultBackgroundColor));
                 layout.addView(textView);
-                addView(layout, LayoutParams.MATCH_PARENT, mainSpacerHeight);
+
+                addView(layout, MATCH_PARENT, mainSpacerHeight);
             }
 
             final int systemUiVisibility = mSettings.getInputViewSystemUiVisibility(0);
@@ -598,6 +629,105 @@
         return new ImeState(hasInputBinding, hasDummyInputConnectionConnection);
     }
 
+    private static LinearLayout sSuggestionView;
+    @GuardedBy("this")
+    private List<View> mSuggestionViews = new ArrayList<>();
+    @GuardedBy("this")
+    private List<Size> mSuggestionViewSizes = new ArrayList<>();
+    @GuardedBy("this")
+    private boolean mSuggestionViewVisible = false;
+
+    public InlineSuggestionsRequest onCreateInlineSuggestionsRequest() {
+        Log.d(TAG, "onCreateInlineSuggestionsRequest() called");
+        final ArrayList<InlinePresentationSpec> presentationSpecs = new ArrayList<>();
+        presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, 100),
+                new Size(400, 100)).build());
+        presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, 100),
+                new Size(400, 100)).build());
+
+        return new InlineSuggestionsRequest.Builder(presentationSpecs)
+                .setMaxSuggestionCount(6)
+                .build();
+    }
+
+    @Override
+    public boolean onInlineSuggestionsResponse(InlineSuggestionsResponse response) {
+        Log.d(TAG, "onInlineSuggestionsResponse() called");
+        AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
+            onInlineSuggestionsResponseInternal(response);
+        });
+        return true;
+    }
+
+    private synchronized void updateInlineSuggestionVisibility(boolean visible, boolean force) {
+        Log.d(TAG, "updateInlineSuggestionVisibility() called, visible=" + visible + ", force="
+                + force);
+        mMainHandler.post(() -> {
+            Log.d(TAG, "updateInlineSuggestionVisibility() running");
+            if (visible == mSuggestionViewVisible && !force) {
+                return;
+            } else if (visible) {
+                sSuggestionView.removeAllViews();
+                final int size = mSuggestionViews.size();
+                for (int i = 0; i < size; i++) {
+                    if(mSuggestionViews.get(i) == null) {
+                        continue;
+                    }
+                    ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
+                            mSuggestionViewSizes.get(i).getWidth(),
+                            mSuggestionViewSizes.get(i).getHeight());
+                    sSuggestionView.addView(mSuggestionViews.get(i), layoutParams);
+                }
+                mSuggestionViewVisible = true;
+            } else {
+                sSuggestionView.removeAllViews();
+                mSuggestionViewVisible = false;
+            }
+        });
+    }
+
+    private void onSuggestionViewUpdated() {
+        getTracer().onSuggestionViewUpdated();
+    }
+
+    private synchronized void updateSuggestionViews(View[] suggestionViews, Size[] sizes) {
+        Log.d(TAG, "updateSuggestionViews() called on " + suggestionViews.length + " views");
+        mSuggestionViews = Arrays.asList(suggestionViews);
+        mSuggestionViewSizes = Arrays.asList(sizes);
+        updateInlineSuggestionVisibility(true, true);
+        onSuggestionViewUpdated();
+    }
+
+    private void onInlineSuggestionsResponseInternal(InlineSuggestionsResponse response) {
+        Log.d(TAG, "onInlineSuggestionsResponseInternal() called. Suggestion="
+                + response.getInlineSuggestions().size());
+
+        final List<InlineSuggestion> inlineSuggestions = response.getInlineSuggestions();
+        final int totalSuggestionsCount = inlineSuggestions.size();
+        final AtomicInteger suggestionsCount = new AtomicInteger(totalSuggestionsCount);
+        final View[] suggestionViews = new View[totalSuggestionsCount];
+        final Size[] sizes = new Size[totalSuggestionsCount];
+
+        for (int i=0; i < totalSuggestionsCount; i++) {
+            final int index = i;
+            InlineSuggestion inlineSuggestion = inlineSuggestions.get(index);
+            Size size = inlineSuggestion.getInfo().getPresentationSpec().getMaxSize();
+            Log.d(TAG, "Calling inflate on suggestion " + i);
+            inlineSuggestion.inflate(this, size,
+                    AsyncTask.THREAD_POOL_EXECUTOR,
+                    suggestionView -> {
+                        Log.d(TAG, "new inline suggestion view ready");
+                        if(suggestionView != null) {
+                            suggestionViews[index] = suggestionView;
+                            sizes[index] = size;
+                        }
+                        if (suggestionsCount.decrementAndGet() == 0) {
+                            updateSuggestionViews(suggestionViews, sizes);
+                        }
+                    });
+        }
+    }
+
     /**
      * Event tracing helper class for {@link MockIme}.
      */
@@ -822,5 +952,9 @@
             imeLayoutInfo.writeToBundle(arguments);
             recordEventInternal("onInputViewLayoutChanged", runnable, arguments);
         }
+
+        public void onSuggestionViewUpdated() {
+            recordEventInternal("onSuggestionViewUpdated", () -> {}, new Bundle());
+        }
     }
 }