import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
+import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
private static final Pattern INPUT_EVENT_PATTERN = Pattern
.compile("/(?<input>\\S+): (?<n1>\\S+) (?<n2>\\S+) (?<n3>\\S+)$", Pattern.MULTILINE);
+ private static final Pattern SECURE_SHELL_INPUT_PATTERN = Pattern.compile("^[^\\|\\&;\\\"]+$");
+
private static @Nullable AdbCrypto adbCrypto;
static {
logger.warn("{} is not a valid package name", packageName);
return;
}
- if (!PACKAGE_NAME_PATTERN.matcher(activityName).matches()) {
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(activityName).matches()) {
logger.warn("{} is not a valid activity name", activityName);
return;
}
public String getMacAddress() throws AndroidDebugBridgeDeviceException, InterruptedException,
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
- return getDeviceProp("ro.boot.wifimacaddr").toLowerCase();
+ return runAdbShell("cat", "/sys/class/net/wlan0/address").replace("\n", "").replace("\r", "");
}
private String getDeviceProp(String name) throws AndroidDebugBridgeDeviceException, InterruptedException,
}
}
+ public void startIntent(String command)
+ throws AndroidDebugBridgeDeviceException, ExecutionException, InterruptedException, TimeoutException {
+ String[] commandParts = command.split("\\|\\|");
+ if (commandParts.length == 0) {
+ throw new AndroidDebugBridgeDeviceException("Empty command");
+ }
+ String targetPackage = commandParts[0];
+ var targetPackageParts = targetPackage.split("/");
+ if (targetPackageParts.length > 2) {
+ throw new AndroidDebugBridgeDeviceException("Invalid target package " + targetPackage);
+ }
+ if (!PACKAGE_NAME_PATTERN.matcher(targetPackageParts[0]).matches()) {
+ logger.warn("{} is not a valid package name", targetPackageParts[0]);
+ return;
+ }
+ if (targetPackageParts.length == 2 && !SECURE_SHELL_INPUT_PATTERN.matcher(targetPackageParts[1]).matches()) {
+ logger.warn("{} is not a valid activity name", targetPackageParts[1]);
+ return;
+ }
+ @Nullable
+ String action = null;
+ @Nullable
+ String dataUri = null;
+ @Nullable
+ String mimeType = null;
+ @Nullable
+ String category = null;
+ @Nullable
+ String component = null;
+ @Nullable
+ String flags = null;
+ Map<String, Boolean> extraBooleans = new HashMap<>();
+ Map<String, String> extraStrings = new HashMap<>();
+ Map<String, Integer> extraIntegers = new HashMap<>();
+ Map<String, Float> extraFloats = new HashMap<>();
+ Map<String, Long> extraLongs = new HashMap<>();
+ Map<String, URI> extraUris = new HashMap<>();
+ for (var i = 1; i < commandParts.length - 1; i++) {
+ var commandPart = commandParts[i];
+ var endToken = commandPart.indexOf(">");
+ var argName = commandPart.substring(0, endToken + 1);
+ var argValue = commandPart.substring(endToken + 1);
+
+ String[] valueParts;
+ switch (argName) {
+ case "<a>":
+ case "<action>":
+ if (!PACKAGE_NAME_PATTERN.matcher(argValue).matches()) {
+ logger.warn("{} is not a valid action name", argValue);
+ return;
+ }
+ action = argValue;
+ break;
+ case "<d>":
+ case "":
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
+ logger.warn("{}, insecure input value", argValue);
+ return;
+ }
+ dataUri = argValue;
+ break;
+ case "<t>":
+ case "<mime_type>":
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
+ logger.warn("{}, insecure input value", argValue);
+ return;
+ }
+ mimeType = argValue;
+ break;
+ case "<c>":
+ case "<category>":
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
+ logger.warn("{}, insecure input value", argValue);
+ return;
+ }
+ category = argValue;
+ break;
+ case "<n>":
+ case "<component>":
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
+ logger.warn("{}, insecure input value", argValue);
+ return;
+ }
+ component = argValue;
+ break;
+ case "<f>":
+ case "<flags>":
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
+ logger.warn("{}, insecure input value", argValue);
+ return;
+ }
+ flags = argValue;
+ break;
+ case "<e>":
+ case "<es>":
+ valueParts = argValue.split(" ");
+ if (valueParts.length != 2) {
+ logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
+ argName, argValue);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[0]);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[1]);
+ return;
+ }
+ extraStrings.put(valueParts[0], valueParts[1]);
+ break;
+ case "<ez>":
+ valueParts = argValue.split(" ");
+ if (valueParts.length != 2) {
+ logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
+ argName, argValue);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[0]);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[1]);
+ return;
+ }
+ extraBooleans.put(valueParts[0], Boolean.parseBoolean(valueParts[1]));
+ break;
+ case "<ei>":
+ valueParts = argValue.split(" ");
+ if (valueParts.length != 2) {
+ logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
+ argName, argValue);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[0]);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[1]);
+ return;
+ }
+ try {
+ extraIntegers.put(valueParts[0], Integer.parseInt(valueParts[1]));
+ } catch (NumberFormatException e) {
+ logger.warn("Unable to parse {} as integer", valueParts[1]);
+ return;
+ }
+ break;
+ case "<el>":
+ valueParts = argValue.split(" ");
+ if (valueParts.length != 2) {
+ logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
+ argName, argValue);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[0]);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[1]);
+ return;
+ }
+ try {
+ extraLongs.put(valueParts[0], Long.parseLong(valueParts[1]));
+ } catch (NumberFormatException e) {
+ logger.warn("Unable to parse {} as long", valueParts[1]);
+ return;
+ }
+ break;
+ case "<ef>":
+ valueParts = argValue.split(" ");
+ if (valueParts.length != 2) {
+ logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
+ argName, argValue);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[0]);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[1]);
+ return;
+ }
+ try {
+ extraFloats.put(valueParts[0], Float.parseFloat(valueParts[1]));
+ } catch (NumberFormatException e) {
+ logger.warn("Unable to parse {} as float", valueParts[1]);
+ return;
+ }
+ break;
+ case "<eu>":
+ valueParts = argValue.split(" ");
+ if (valueParts.length != 2) {
+ logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
+ argName, argValue);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[0]);
+ return;
+ }
+ if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
+ logger.warn("{}, insecure input value", valueParts[1]);
+ return;
+ }
+ extraUris.put(valueParts[0], URI.create(valueParts[1]));
+ break;
+ default:
+ throw new AndroidDebugBridgeDeviceException("Unsupported arg " + argName
+ + ". Open an issue or pr for it if you think support should be added.");
+ }
+ }
+
+ StringBuilder adbCommandBuilder = new StringBuilder("am start " + targetPackage);
+ if (action != null) {
+ adbCommandBuilder.append(" -a ").append(action);
+ }
+ if (dataUri != null) {
+ adbCommandBuilder.append(" -d ").append(dataUri);
+ }
+ if (mimeType != null) {
+ adbCommandBuilder.append(" -t ").append(mimeType);
+ }
+ if (category != null) {
+ adbCommandBuilder.append(" -c ").append(category);
+ }
+ if (component != null) {
+ adbCommandBuilder.append(" -n ").append(component);
+ }
+ if (flags != null) {
+ adbCommandBuilder.append(" -f ").append(flags);
+ }
+ if (!extraStrings.isEmpty()) {
+ adbCommandBuilder.append(extraStrings.entrySet().stream()
+ .map(entry -> " --es \"" + entry.getKey() + "\" \"" + entry.getValue() + "\""));
+ }
+ if (!extraBooleans.isEmpty()) {
+ adbCommandBuilder.append(extraBooleans.entrySet().stream()
+ .map(entry -> " --ez \"" + entry.getKey() + "\" " + entry.getValue()));
+ }
+ if (!extraIntegers.isEmpty()) {
+ adbCommandBuilder.append(extraIntegers.entrySet().stream()
+ .map(entry -> " --ei \"" + entry.getKey() + "\" " + entry.getValue()));
+ }
+ if (!extraFloats.isEmpty()) {
+ adbCommandBuilder.append(extraFloats.entrySet().stream()
+ .map(entry -> " --ef \"" + entry.getKey() + "\" " + entry.getValue()));
+ }
+ if (!extraLongs.isEmpty()) {
+ adbCommandBuilder.append(extraLongs.entrySet().stream()
+ .map(entry -> " --el \"" + entry.getKey() + "\" " + entry.getValue()));
+ }
+ runAdbShell(adbCommandBuilder.toString());
+ }
+
public boolean isConnected() {
var currentSocket = socket;
return currentSocket != null && currentSocket.isConnected();
thing-type.config.androiddebugbridge.android.mediaStateJSONConfig.description = JSON config that allows to modify the media state detection strategy for each app. Refer to the binding documentation.
thing-type.config.androiddebugbridge.android.port.label = Port
thing-type.config.androiddebugbridge.android.port.description = Device port listening to adb connections.
+thing-type.config.androiddebugbridge.android.recordDuration.label = Record Duration
+thing-type.config.androiddebugbridge.android.recordDuration.description = How much time the record-input channel wait for events to record.
thing-type.config.androiddebugbridge.android.refreshTime.label = Refresh Time
thing-type.config.androiddebugbridge.android.refreshTime.description = Seconds between device status refreshes.
thing-type.config.androiddebugbridge.android.timeout.label = Command Timeout
channel-type.androiddebugbridge.key-event-channel.state.option.211 = KEYCODE_ZENKAKU_HANKAKU
channel-type.androiddebugbridge.key-event-channel.state.option.168 = KEYCODE_ZOOM_IN
channel-type.androiddebugbridge.key-event-channel.state.option.16 = KEYCODE_ZOOM_OUT
+channel-type.androiddebugbridge.record-input-channel.label = Record Input
+channel-type.androiddebugbridge.record-input-channel.description = Record input events under provided name
+channel-type.androiddebugbridge.recorded-input-channel.label = Recorded Input
+channel-type.androiddebugbridge.recorded-input-channel.description = Send previous recorded input events by name
channel-type.androiddebugbridge.screen-state-channel.label = Screen State
channel-type.androiddebugbridge.screen-state-channel.description = Screen Power State
channel-type.androiddebugbridge.shutdown-channel.label = Shutdown
channel-type.androiddebugbridge.shutdown-channel.description = Shutdown/Reboot Device
channel-type.androiddebugbridge.shutdown-channel.state.option.POWER_OFF = POWER_OFF
channel-type.androiddebugbridge.shutdown-channel.state.option.REBOOT = REBOOT
+channel-type.androiddebugbridge.start-intent-channel.label = Start Intent
+channel-type.androiddebugbridge.start-intent-channel.description = Start application intent
channel-type.androiddebugbridge.start-package-channel.label = Start Package
channel-type.androiddebugbridge.start-package-channel.description = Run application by package name
channel-type.androiddebugbridge.stop-current-package-channel.label = Stop Current Package
channel-type.androiddebugbridge.tap-channel.description = Send tap event to android device
channel-type.androiddebugbridge.text-channel.label = Send Text
channel-type.androiddebugbridge.text-channel.description = Send text to android device
+channel-type.androiddebugbridge.url-channel.label = Open Url
+channel-type.androiddebugbridge.url-channel.description = Open url in the browser
channel-type.androiddebugbridge.wake-lock-channel.label = Wake Lock
channel-type.androiddebugbridge.wake-lock-channel.description = Power Wake Lock State