blob: 21482ea6ff79f9d50d587fde123e093d248ee4f4 [file] [log] [blame]
/*
* 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 com.android.server.timezonedetector.location;
import android.annotation.NonNull;
import android.net.Uri;
import android.os.Bundle;
import android.os.ShellCommand;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A command used to trigger behaviors in a component during tests. Routing to the correct
* component is not handled by this class. The meaning of the {@code name} and {@code args}
* properties are component-specific.
*
* <p>{@link TestCommand}s can be encoded as arguments in a shell command. See
* {@link #createFromShellCommandArgs(ShellCommand)} and {@link
* #printShellCommandEncodingHelp(PrintWriter)}.
*/
final class TestCommand {
private static final Pattern SHELL_ARG_PATTERN = Pattern.compile("([^=]+)=([^:]+):(.*)");
private static final Pattern SHELL_ARG_VALUE_SPLIT_PATTERN = Pattern.compile("&");
@NonNull private final String mName;
@NonNull private final Bundle mArgs;
/** Creates a {@link TestCommand} from components. */
private TestCommand(@NonNull String type, @NonNull Bundle args) {
mName = Objects.requireNonNull(type);
mArgs = Objects.requireNonNull(args);
}
@VisibleForTesting
@NonNull
public static TestCommand createForTests(@NonNull String type, @NonNull Bundle args) {
return new TestCommand(type, args);
}
/**
* Creates a {@link TestCommand} from a {@link ShellCommand}'s remaining arguments.
*
* See {@link #printShellCommandEncodingHelp(PrintWriter)} for encoding details.
*/
@NonNull
public static TestCommand createFromShellCommandArgs(@NonNull ShellCommand shellCommand) {
String name = shellCommand.getNextArgRequired();
Bundle args = new Bundle();
String argKeyAndValue;
while ((argKeyAndValue = shellCommand.getNextArg()) != null) {
Matcher matcher = SHELL_ARG_PATTERN.matcher(argKeyAndValue);
if (!matcher.matches()) {
throw new IllegalArgumentException(
argKeyAndValue + " does not match " + SHELL_ARG_PATTERN);
}
String key = matcher.group(1);
String type = matcher.group(2);
String encodedValue = matcher.group(3);
Object value = getTypedValue(type, encodedValue);
args.putObject(key, value);
}
return new TestCommand(name, args);
}
/**
* Returns the command's name.
*/
@NonNull
public String getName() {
return mName;
}
/**
* Returns the arg values. Returns an empty bundle if there are no args.
*/
@NonNull
public Bundle getArgs() {
return mArgs.deepCopy();
}
@Override
public String toString() {
return "TestCommand{"
+ "mName=" + mName
+ ", mArgs=" + mArgs
+ '}';
}
/**
* Prints the text format that {@link #createFromShellCommandArgs(ShellCommand)} understands.
*/
public static void printShellCommandEncodingHelp(@NonNull PrintWriter pw) {
pw.println("Test commands are encoded on the command line as: <name> <arg>*");
pw.println();
pw.println("The <name> is a string");
pw.println("The <arg> encoding is: \"key=type:value\"");
pw.println();
pw.println("e.g. \"myKey=string:myValue\" represents an argument with the key \"myKey\""
+ " and a string value of \"myValue\"");
pw.println("Values are one or more URI-encoded strings separated by & characters. Only some"
+ " types support multiple values, e.g. string arrays.");
pw.println();
pw.println("Recognized types are: string, boolean, double, long, string_array.");
pw.println();
pw.println("When passing test commands via adb shell, the & can be escaped by quoting the"
+ " <arg> and escaping the & with \\");
pw.println("For example:");
pw.println(" $ adb shell ... my-command \"key1=string_array:value1\\&value2\"");
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TestCommand that = (TestCommand) o;
return mName.equals(that.mName)
&& mArgs.kindofEquals(that.mArgs);
}
@Override
public int hashCode() {
return Objects.hash(mName, mArgs);
}
private static Object getTypedValue(String type, String encodedValue) {
// The value is stored in a URL encoding. Multiple value types have values separated with
// a & character.
String[] values = SHELL_ARG_VALUE_SPLIT_PATTERN.split(encodedValue);
// URI decode the values.
for (int i = 0; i < values.length; i++) {
values[i] = Uri.decode(values[i]);
}
switch (type) {
case "boolean": {
checkSingleValue(values);
return Boolean.parseBoolean(values[0]);
}
case "double": {
checkSingleValue(values);
return Double.parseDouble(values[0]);
}
case "long": {
checkSingleValue(values);
return Long.parseLong(values[0]);
}
case "string": {
checkSingleValue(values);
return values[0];
}
case "string_array": {
return values;
}
default: {
throw new IllegalArgumentException("Unknown type: " + type);
}
}
}
private static void checkSingleValue(String[] values) {
if (values.length != 1) {
throw new IllegalArgumentException("Expected a single value, but there were multiple: "
+ Arrays.toString(values));
}
}
}