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());
+ }
}
}