diff --git a/bundles/org.openhab.binding.androiddebugbridge/README.md b/bundles/org.openhab.binding.androiddebugbridge/README.md index 5d9167b8bd933..b260750239622 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/README.md +++ b/bundles/org.openhab.binding.androiddebugbridge/README.md @@ -90,6 +90,7 @@ Please note that events could fail if the input method is removed, for example i | url | String | Open url in browser | | media-volume | Dimmer | Set or get media volume level on android device | | media-control | Player | Control media on android device | +| start-intent | String | Start application intent. Read bellow section | | start-package | String | Run application by package name. The commands for this Channel are populated dynamically based on the `mediaStateJSONConfig`. | | stop-package | String | Stop application by package name | | stop-current-package | String | Stop current application | @@ -101,6 +102,16 @@ Please note that events could fail if the input method is removed, for example i | wake-lock | Number | Power wake lock value | | screen-state | Switch | Screen power state | +#### Start Intent + +This channel allows to invoke the 'am start' command, the syntax for it is: +||<> ||... + +This is a sample: +com.netflix.ninja/.MainActivity||android.intent.action.VIEW||netflix://title/80025384||0x10000020||amzn_deeplink_data 80025384 + +Not all the (arguments)[https://developer.android.com/studio/command-line/adb#IntentSpec] are supported. Please open an issue or pull request if you need more. + #### Available key-event values: * KEYCODE_0 diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java index c831e123aca4c..a9ea6efd7f25e 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java @@ -33,6 +33,7 @@ public class AndroidDebugBridgeBindingConstants { public static final ThingTypeUID THING_TYPE_ANDROID_DEVICE = new ThingTypeUID(BINDING_ID, "android"); public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_ANDROID_DEVICE); // List of all Channel ids + public static final String START_INTENT_CHANNEL = "start-intent"; public static final String KEY_EVENT_CHANNEL = "key-event"; public static final String TEXT_CHANNEL = "text"; public static final String TAP_CHANNEL = "tap"; diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java index 0fbba7202cbd3..7092d840f91e6 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java @@ -17,6 +17,7 @@ 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; @@ -25,6 +26,8 @@ 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; @@ -65,6 +68,8 @@ public class AndroidDebugBridgeDevice { private static final Pattern INPUT_EVENT_PATTERN = Pattern .compile("/(?\\S+): (?\\S+) (?\\S+) (?\\S+)$", Pattern.MULTILINE); + private static final Pattern SECURE_SHELL_INPUT_PATTERN = Pattern.compile("^[^\\|\\&;\\\"]+$"); + private static @Nullable AdbCrypto adbCrypto; static { @@ -170,7 +175,7 @@ public void startPackageWithActivity(String packageWithActivity) 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; } @@ -299,7 +304,7 @@ public String getSerialNo() throws AndroidDebugBridgeDeviceException, Interrupte 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, @@ -374,6 +379,265 @@ public void powerOffDevice() } } + 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 extraBooleans = new HashMap<>(); + Map extraStrings = new HashMap<>(); + Map extraIntegers = new HashMap<>(); + Map extraFloats = new HashMap<>(); + Map extraLongs = new HashMap<>(); + Map 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 "": + case "": + if (!PACKAGE_NAME_PATTERN.matcher(argValue).matches()) { + logger.warn("{} is not a valid action name", argValue); + return; + } + action = argValue; + break; + case "": + case "": + if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) { + logger.warn("{}, insecure input value", argValue); + return; + } + dataUri = argValue; + break; + case "": + case "": + if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) { + logger.warn("{}, insecure input value", argValue); + return; + } + mimeType = argValue; + break; + case "": + case "": + if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) { + logger.warn("{}, insecure input value", argValue); + return; + } + category = argValue; + break; + case "": + case "": + if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) { + logger.warn("{}, insecure input value", argValue); + return; + } + component = argValue; + break; + case "": + case "": + if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) { + logger.warn("{}, insecure input value", argValue); + return; + } + flags = argValue; + break; + case "": + case "": + 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 "": + 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 "": + 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 "": + 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 "": + 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 "": + 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(); diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java index c442ebfca5246..25c13a5d1e7e6 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java @@ -180,6 +180,12 @@ private void handleCommandInternal(ChannelUID channelUID, Command command) break; } break; + case START_INTENT_CHANNEL: + if (command instanceof RefreshType) { + return; + } + adbConnection.startIntent(command.toFullString()); + break; case RECORD_INPUT_CHANNEL: recordDeviceInput(command); break; diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/i18n/androiddebugbridge.properties b/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/i18n/androiddebugbridge.properties index 7e336f9ff10d7..4556f4cd8b740 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/i18n/androiddebugbridge.properties +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/i18n/androiddebugbridge.properties @@ -27,6 +27,8 @@ thing-type.config.androiddebugbridge.android.mediaStateJSONConfig.label = Media 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 @@ -329,12 +331,18 @@ channel-type.androiddebugbridge.key-event-channel.state.option.54 = KEYCODE_Z 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 @@ -345,5 +353,7 @@ channel-type.androiddebugbridge.tap-channel.label = Send Tap 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 diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml index 94fc30c383a6a..79d8176741893 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml @@ -24,6 +24,7 @@ + macAddress @@ -447,4 +448,10 @@ + + String + + Start application intent + +