blob: 9e290ba2aacca9a37d39579d30757002ac46d79c [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.autofillservice.cts;
import static android.autofillservice.cts.Helper.runShellCommand;
import static android.provider.Settings.Secure.AUTOFILL_SERVICE;
import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.app.assist.AssistStructure;
import android.app.assist.AssistStructure.ViewNode;
import android.app.assist.AssistStructure.WindowNode;
import android.content.Context;
import android.content.pm.PackageManager;
import android.icu.util.Calendar;
import android.os.UserManager;
import android.service.autofill.FillContext;
import android.support.annotation.NonNull;
import android.support.test.InstrumentationRegistry;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewStructure.HtmlInfo;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import com.android.compatibility.common.util.SystemUtil;
import java.util.List;
import java.util.function.Function;
/**
* Helper for common funcionalities.
*/
final class Helper {
private static final String TAG = "AutoFillCtsHelper";
static final boolean VERBOSE = false;
static final String ID_USERNAME_LABEL = "username_label";
static final String ID_USERNAME = "username";
static final String ID_PASSWORD_LABEL = "password_label";
static final String ID_PASSWORD = "password";
static final String ID_LOGIN = "login";
static final String ID_OUTPUT = "output";
/** Pass to {@link #setOrientation(int)} to change the display to portrait mode */
public static int PORTRAIT = 0;
/** Pass to {@link #setOrientation(int)} to change the display to landscape mode */
public static int LANDSCAPE = 1;
/**
* Timeout (in milliseconds) until framework binds / unbinds from service.
*/
static final long CONNECTION_TIMEOUT_MS = 2000;
/**
* Timeout (in milliseconds) until framework unbinds from a service.
*/
static final long IDLE_UNBIND_TIMEOUT_MS = 5000;
/**
* Timeout (in milliseconds) for expected auto-fill requests.
*/
static final long FILL_TIMEOUT_MS = 2000;
/**
* Timeout (in milliseconds) for expected save requests.
*/
static final long SAVE_TIMEOUT_MS = 5000;
/**
* Time to wait if a UI change is not expected
*/
static final long NOT_SHOWING_TIMEOUT_MS = 500;
/**
* Timeout (in milliseconds) for UI operations. Typically used by {@link UiBot}.
*/
static final int UI_TIMEOUT_MS = 2000;
/**
* Time to wait in between retries
*/
static final int RETRY_MS = 100;
private final static String ACCELLEROMETER_CHANGE =
"content insert --uri content://settings/system --bind name:s:accelerometer_rotation "
+ "--bind value:i:%d";
private final static String ORIENTATION_CHANGE =
"content insert --uri content://settings/system --bind name:s:user_rotation --bind "
+ "value:i:%d";
/**
* Runs a {@code r}, ignoring all {@link RuntimeException} and {@link Error} until the
* {@link #UI_TIMEOUT_MS} is reached.
*/
static void eventually(Runnable r) throws Exception {
eventually(r, UI_TIMEOUT_MS);
}
/**
* Runs a {@code r}, ignoring all {@link RuntimeException} and {@link Error} until the
* {@code timeout} is reached.
*/
static void eventually(Runnable r, int timeout) throws Exception {
long startTime = System.currentTimeMillis();
while (true) {
try {
r.run();
break;
} catch (RuntimeException | Error e) {
if (System.currentTimeMillis() - startTime < timeout) {
if (VERBOSE) Log.v(TAG, "Ignoring", e);
Thread.sleep(RETRY_MS);
} else {
if (e instanceof RetryableException) {
throw e;
} else {
throw new Exception("Timedout out after " + timeout + " ms", e);
}
}
}
}
}
/**
* Runs a Shell command, returning a trimmed response.
*/
static String runShellCommand(String template, Object...args) {
final String command = String.format(template, args);
Log.d(TAG, "runShellCommand(): " + command);
try {
final String result = SystemUtil
.runShellCommand(InstrumentationRegistry.getInstrumentation(), command);
return TextUtils.isEmpty(result) ? "" : result.trim();
} catch (Exception e) {
throw new RuntimeException("Command '" + command + "' failed: ", e);
}
}
/**
* 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());
}
/**
* Dump the contexts on logcat.
*/
static void dumpStructure(String message, List<FillContext> contexts) {
for (FillContext context : contexts) {
dumpStructure(message, context.getStructure());
}
}
/**
* Dumps the state of the autofill service on logcat.
*/
static void dumpAutofillService() {
Log.i(TAG, "dumpsys autofill\n\n" + runShellCommand("dumpsys autofill"));
}
/**
* Sets the {@link UserManager#DISALLOW_AUTOFILL} for the current user.
*/
static void setUserRestrictionForAutofill(boolean restricted) {
runShellCommand("pm set-user-restriction no_autofill %d", restricted ? 1 : 0);
}
/**
* Sets whether the user completed the initial setup.
*/
static void setUserComplete(Context context, boolean complete) {
if (isUserComplete() == complete) return;
final OneTimeSettingsListener observer = new OneTimeSettingsListener(context,
USER_SETUP_COMPLETE);
final String newValue = complete ? "1" : null;
runShellCommand("settings put secure %s %s default", USER_SETUP_COMPLETE, newValue);
observer.assertCalled();
assertIsUserComplete(complete);
}
/**
* Gets whether the user completed the initial setup.
*/
static boolean isUserComplete() {
final String isIt = runShellCommand("settings get secure %s", USER_SETUP_COMPLETE);
return "1".equals(isIt);
}
/**
* Assets that user completed (or not) the initial setup.
*/
static void assertIsUserComplete(boolean expected) {
final boolean actual = isUserComplete();
assertWithMessage("Invalid value for secure setting %s", USER_SETUP_COMPLETE)
.that(actual).isEqualTo(expected);
}
private static void dump(StringBuffer buffer, ViewNode node, 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(" #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());
final HtmlInfo htmlInfo = node.getHtmlInfo();
if (htmlInfo != null) {
buffer.append("\nHtmlInfo: tag=").append(htmlInfo.getTag())
.append(", attrs: ").append(htmlInfo.getAttributes());
}
prefix += " ";
if (childrenSize > 0) {
for (int i = 0; i < childrenSize; i++) {
dump(buffer, node.getChildAt(i), prefix, i);
}
}
}
/**
* Gets a node given its Android resource id, or {@code null} if not found.
*/
static ViewNode findNodeByResourceId(AssistStructure structure, String resourceId) {
Log.v(TAG, "Parsing request for activity " + structure.getActivityComponent());
final int nodes = structure.getWindowNodeCount();
for (int i = 0; i < nodes; i++) {
final WindowNode windowNode = structure.getWindowNodeAt(i);
final ViewNode rootNode = windowNode.getRootViewNode();
final ViewNode node = findNodeByResourceId(rootNode, resourceId);
if (node != null) {
return node;
}
}
return null;
}
/**
* Gets a node given its Android resource id, or {@code null} if not found.
*/
static ViewNode findNodeByResourceId(List<FillContext> contexts, String resourceId) {
for (FillContext context : contexts) {
ViewNode node = findNodeByResourceId(context.getStructure(), resourceId);
if (node != null) {
return node;
}
}
return null;
}
/**
* Gets a node given its Android resource id, or {@code null} if not found.
*/
static ViewNode findNodeByResourceId(ViewNode node, String resourceId) {
if (resourceId.equals(node.getIdEntry())) {
return node;
}
final int childrenSize = node.getChildCount();
if (childrenSize > 0) {
for (int i = 0; i < childrenSize; i++) {
final ViewNode found = findNodeByResourceId(node.getChildAt(i), resourceId);
if (found != null) {
return found;
}
}
}
return null;
}
/**
* Gets a node given its expected text, or {@code null} if not found.
*/
static ViewNode findNodeByText(AssistStructure structure, String text) {
Log.v(TAG, "Parsing request for activity " + structure.getActivityComponent());
final int nodes = structure.getWindowNodeCount();
for (int i = 0; i < nodes; i++) {
final WindowNode windowNode = structure.getWindowNodeAt(i);
final ViewNode rootNode = windowNode.getRootViewNode();
final ViewNode node = findNodeByText(rootNode, text);
if (node != null) {
return node;
}
}
return null;
}
/**
* Gets a node given its expected text, or {@code null} if not found.
*/
static ViewNode findNodeByText(ViewNode node, String text) {
if (text.equals(node.getText())) {
return node;
}
final int childrenSize = node.getChildCount();
if (childrenSize > 0) {
for (int i = 0; i < childrenSize; i++) {
final ViewNode found = findNodeByText(node.getChildAt(i), text);
if (found != null) {
return found;
}
}
}
return null;
}
/**
* Asserts a text-base node is sanitized.
*/
static void assertTextIsSanitized(ViewNode node) {
final CharSequence text = node.getText();
final String resourceId = node.getIdEntry();
if (!TextUtils.isEmpty(text)) {
throw new AssertionError("text on sanitized field " + resourceId + ": " + text);
}
assertNodeHasNoAutofillValue(node);
}
static void assertNodeHasNoAutofillValue(ViewNode node) {
final AutofillValue value = node.getAutofillValue();
if (value != null) {
final String text = value.isText() ? value.getTextValue().toString() : "N/A";
throw new AssertionError("node has value: " + value + " text=" + text);
}
}
/**
* Asserts the contents of a text-based node that is also auto-fillable.
*
*/
static void assertTextOnly(ViewNode node, String expectedValue) {
assertText(node, expectedValue, false);
}
/**
* Asserts the contents of a text-based node that is also auto-fillable.
*
*/
static void assertTextAndValue(ViewNode node, String expectedValue) {
assertText(node, expectedValue, true);
}
/**
* Asserts a text-base node exists and verify its values.
*/
static ViewNode assertTextAndValue(AssistStructure structure, String resourceId,
String expectedValue) {
final ViewNode node = findNodeByResourceId(structure, resourceId);
assertTextAndValue(node, expectedValue);
return node;
}
/**
* Asserts a text-base node exists and is sanitized.
*/
static ViewNode assertValue(AssistStructure structure, String resourceId,
String expectedValue) {
final ViewNode node = findNodeByResourceId(structure, resourceId);
assertTextValue(node, expectedValue);
return node;
}
private static void assertText(ViewNode node, String expectedValue, boolean isAutofillable) {
assertWithMessage("wrong text on %s", node).that(node.getText().toString())
.isEqualTo(expectedValue);
final AutofillValue value = node.getAutofillValue();
if (isAutofillable) {
assertWithMessage("null auto-fill value on %s", node).that(value).isNotNull();
assertWithMessage("wrong auto-fill value on %s", node)
.that(value.getTextValue().toString()).isEqualTo(expectedValue);
} else {
assertWithMessage("node %s should not have AutofillValue", node).that(value).isNull();
}
}
/**
* Asserts the auto-fill value of a text-based node.
*/
static ViewNode assertTextValue(ViewNode node, String expectedText) {
final AutofillValue value = node.getAutofillValue();
assertWithMessage("null autofill value on %s", node).that(value).isNotNull();
assertWithMessage("wrong autofill type on %s", node).that(value.isText()).isTrue();
assertWithMessage("wrong autofill value on %s", node).that(value.getTextValue().toString())
.isEqualTo(expectedText);
return node;
}
/**
* Asserts the auto-fill value of a list-based node.
*/
static ViewNode assertListValue(ViewNode node, int expectedIndex) {
final AutofillValue value = node.getAutofillValue();
assertWithMessage("null autofill value on %s", node).that(value).isNotNull();
assertWithMessage("wrong autofill type on %s", node).that(value.isList()).isTrue();
assertWithMessage("wrong autofill value on %s", node).that(value.getListValue())
.isEqualTo(expectedIndex);
return node;
}
/**
* Asserts the auto-fill value of a toggle-based node.
*/
static void assertToggleValue(ViewNode node, boolean expectedToggle) {
final AutofillValue value = node.getAutofillValue();
assertWithMessage("null autofill value on %s", node).that(value).isNotNull();
assertWithMessage("wrong autofill type on %s", node).that(value.isToggle()).isTrue();
assertWithMessage("wrong autofill value on %s", node).that(value.getToggleValue())
.isEqualTo(expectedToggle);
}
/**
* Asserts the auto-fill value of a date-based node.
*/
static void assertDateValue(Object object, AutofillValue value, int year, int month, int day) {
assertWithMessage("null autofill value on %s", object).that(value).isNotNull();
assertWithMessage("wrong autofill type on %s", object).that(value.isDate()).isTrue();
final Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(value.getDateValue());
assertWithMessage("Wrong year on AutofillValue %s", value)
.that(cal.get(Calendar.YEAR)).isEqualTo(year);
assertWithMessage("Wrong month on AutofillValue %s", value)
.that(cal.get(Calendar.MONTH)).isEqualTo(month);
assertWithMessage("Wrong day on AutofillValue %s", value)
.that(cal.get(Calendar.DAY_OF_MONTH)).isEqualTo(day);
}
/**
* Asserts the auto-fill value of a date-based node.
*/
static void assertDateValue(ViewNode node, int year, int month, int day) {
assertDateValue(node, node.getAutofillValue(), year, month, day);
}
/**
* Asserts the auto-fill value of a date-based view.
*/
static void assertDateValue(View view, int year, int month, int day) {
assertDateValue(view, view.getAutofillValue(), year, month, day);
}
/**
* Asserts the auto-fill value of a time-based node.
*/
private static void assertTimeValue(Object object, AutofillValue value, int hour, int minute) {
assertWithMessage("null autofill value on %s", object).that(value).isNotNull();
assertWithMessage("wrong autofill type on %s", object).that(value.isDate()).isTrue();
final Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(value.getDateValue());
assertWithMessage("Wrong hour on AutofillValue %s", value)
.that(cal.get(Calendar.HOUR_OF_DAY)).isEqualTo(hour);
assertWithMessage("Wrong minute on AutofillValue %s", value)
.that(cal.get(Calendar.MINUTE)).isEqualTo(minute);
}
/**
* Asserts the auto-fill value of a time-based node.
*/
static void assertTimeValue(ViewNode node, int hour, int minute) {
assertTimeValue(node, node.getAutofillValue(), hour, minute);
}
/**
* Asserts the auto-fill value of a time-based view.
*/
static void assertTimeValue(View view, int hour, int minute) {
assertTimeValue(view, view.getAutofillValue(), hour, minute);
}
/**
* Asserts a text-base node exists and is sanitized.
*/
static ViewNode assertTextIsSanitized(AssistStructure structure, String resourceId) {
final ViewNode node = findNodeByResourceId(structure, resourceId);
assertWithMessage("no ViewNode with id %s", resourceId).that(node).isNotNull();
assertTextIsSanitized(node);
return node;
}
/**
* Asserts a list-based node exists and is sanitized.
*/
static void assertListValueIsSanitized(AssistStructure structure, String resourceId) {
final ViewNode node = findNodeByResourceId(structure, resourceId);
assertWithMessage("no ViewNode with id %s", resourceId).that(node).isNotNull();
assertTextIsSanitized(node);
}
/**
* Asserts a toggle node exists and is sanitized.
*/
static void assertToggleIsSanitized(AssistStructure structure, String resourceId) {
final ViewNode node = findNodeByResourceId(structure, resourceId);
assertNodeHasNoAutofillValue(node);
assertWithMessage("ViewNode %s should not be checked", resourceId).that(node.isChecked())
.isFalse();
}
/**
* Asserts a node exists and has the {@code expected} number of children.
*/
static void assertNumberOfChildren(AssistStructure structure, String resourceId, int expected) {
final ViewNode node = findNodeByResourceId(structure, resourceId);
final int actual = node.getChildCount();
if (actual != expected) {
dumpStructure("assertNumberOfChildren()", structure);
throw new AssertionError("assertNumberOfChildren() for " + resourceId
+ " failed: expected " + expected + ", got " + actual);
}
}
/**
* Asserts the number of children in the Assist structure.
*/
static void assertNumberOfChildren(AssistStructure structure, int expected) {
assertWithMessage("wrong number of nodes").that(structure.getWindowNodeCount())
.isEqualTo(1);
final int actual = getNumberNodes(structure);
if (actual != expected) {
dumpStructure("assertNumberOfChildren()", structure);
throw new AssertionError("assertNumberOfChildren() for structure failed: expected "
+ expected + ", got " + actual);
}
}
/**
* Gets the total number of nodes in an structure, including all descendants.
*/
static int getNumberNodes(AssistStructure structure) {
int count = 0;
final int nodes = structure.getWindowNodeCount();
for (int i = 0; i < nodes; i++) {
final WindowNode windowNode = structure.getWindowNodeAt(i);
final ViewNode rootNode = windowNode.getRootViewNode();
count += getNumberNodes(rootNode);
}
return count;
}
/**
* Gets the total number of nodes in an node, including all descendants and the node itself.
*/
private static int getNumberNodes(ViewNode node) {
int count = 1;
final int childrenSize = node.getChildCount();
if (childrenSize > 0) {
for (int i = 0; i < childrenSize; i++) {
count += getNumberNodes(node.getChildAt(i));
}
}
return count;
}
/**
* Creates an array of {@link AutofillId} mapped from the {@code structure} nodes with the given
* {@code resourceIds}.
*/
static AutofillId[] getAutofillIds(Function<String, ViewNode> nodeResolver,
String[] resourceIds) {
if (resourceIds == null) return null;
final AutofillId[] requiredIds = new AutofillId[resourceIds.length];
for (int i = 0; i < resourceIds.length; i++) {
final String resourceId = resourceIds[i];
final ViewNode node = nodeResolver.apply(resourceId);
if (node == null) {
throw new AssertionError("No node with savable resourceId " + resourceId);
}
requiredIds[i] = node.getAutofillId();
}
return requiredIds;
}
/**
* Prevents the screen to rotate by itself
*/
public static void disableAutoRotation() {
runShellCommand(ACCELLEROMETER_CHANGE, 0);
setOrientation(PORTRAIT);
}
/**
* Allows the screen to rotate by itself
*/
public static void allowAutoRotation() {
runShellCommand(ACCELLEROMETER_CHANGE, 1);
}
/**
* Changes the screen orientation. This triggers a activity lifecycle (destroy -> create) for
* activities that do not handle this config change such as {@link OutOfProcessLoginActivity}.
*
* @param value {@link #PORTRAIT} or {@link #LANDSCAPE};
*/
public static void setOrientation(int value) {
runShellCommand(ORIENTATION_CHANGE, value);
}
/**
* Wait until a process starts and returns the process ID of the process.
*
* @return The pid of the process
*/
public static int getOutOfProcessPid(@NonNull String processName) throws InterruptedException {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < UI_TIMEOUT_MS) {
String[] allProcessDescs = runShellCommand("ps -eo PID,ARGS=CMD").split("\n");
for (String processDesc : allProcessDescs) {
String[] pidAndName = processDesc.trim().split(" ");
if (pidAndName[1].equals(processName)) {
return Integer.parseInt(pidAndName[0]);
}
}
Thread.sleep(RETRY_MS);
}
throw new IllegalStateException("process not found");
}
/**
* Gets the maximum number of partitions per session.
*/
public static int getMaxPartitions() {
return Integer.parseInt(runShellCommand("cmd autofill get max_partitions"));
}
/**
* Sets the maximum number of partitions per session.
*/
public static void setMaxPartitions(int value) {
runShellCommand("cmd autofill set max_partitions %d", value);
assertThat(getMaxPartitions()).isEqualTo(value);
}
/**
* Checks if device supports the Autofill feature.
*/
public static boolean hasAutofillFeature() {
return RequiredFeatureRule.hasFeature(PackageManager.FEATURE_AUTOFILL);
}
private Helper() {
}
}