]> git.basschouten.com Git - openhab-addons.git/commitdiff
[androiddebugbridge] add start intent channel (#12438)
authorGiviMAD <GiviMAD@users.noreply.github.com>
Sat, 30 Apr 2022 18:32:11 +0000 (20:32 +0200)
committerGitHub <noreply@github.com>
Sat, 30 Apr 2022 18:32:11 +0000 (20:32 +0200)
* [androiddebugbridge] add start intent channel

Signed-off-by: Miguel Álvarez Díez <miguelwork92@gmail.com>
bundles/org.openhab.binding.androiddebugbridge/README.md
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java
bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/i18n/androiddebugbridge.properties
bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml

index 5d9167b8bd933460f276dea5c56d52dcc7ce07ab..b2607502396223bf8dcb1fe5e3c79aaee82e8083 100644 (file)
@@ -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:
+<package/activity>||<<arg name>> <arg value>||...
+
+This is a sample:
+com.netflix.ninja/.MainActivity||<a>android.intent.action.VIEW||<d>netflix://title/80025384||<f>0x10000020||<es>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
index c831e123aca4cc0df63cd2c04af1fca7f0c51c25..a9ea6efd7f25e613f7018aa6319a43807acdacf4 100644 (file)
@@ -33,6 +33,7 @@ public class AndroidDebugBridgeBindingConstants {
     public static final ThingTypeUID THING_TYPE_ANDROID_DEVICE = new ThingTypeUID(BINDING_ID, "android");
     public static final Set<ThingTypeUID> 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";
index 0fbba7202cbd39be183044b8ec4b66ed0c600897..7092d840f91e672f68bb2ec114682f6dddda25a0 100644 (file)
@@ -17,6 +17,7 @@ import java.io.File;
 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.security.spec.InvalidKeySpecException;
 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("/(?<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 {
@@ -170,7 +175,7 @@ public class AndroidDebugBridgeDevice {
             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 class AndroidDebugBridgeDevice {
 
     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 class AndroidDebugBridgeDevice {
         }
     }
 
+    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();
index c442ebfca5246fc9c22cb3f70dc5ef7c633f9ea8..25c13a5d1e7e6d7a3cf7b059d219919ee487a159 100644 (file)
@@ -180,6 +180,12 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
                         break;
                 }
                 break;
+            case START_INTENT_CHANNEL:
+                if (command instanceof RefreshType) {
+                    return;
+                }
+                adbConnection.startIntent(command.toFullString());
+                break;
             case RECORD_INPUT_CHANNEL:
                 recordDeviceInput(command);
                 break;
index 7e336f9ff10d79fc4a2c55cb2ffef6033f5fed0a..4556f4cd8b740ef24a7f355613f1c4adbddc228a 100644 (file)
@@ -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
index 94fc30c383a6a48aa8a22e41d79730ae6452d6d9..79d817674189374785c63a431c81e5ff588a468a 100644 (file)
@@ -24,6 +24,7 @@
                        <channel id="screen-state" typeId="screen-state-channel"/>
                        <channel id="shutdown" typeId="shutdown-channel"/>
                        <channel id="awake-state" typeId="awake-state-channel"/>
+                       <channel id="start-intent" typeId="start-intent-channel"/>
                </channels>
                <representation-property>macAddress</representation-property>
                <config-description>
                <state readOnly="true"/>
        </channel-type>
 
+       <channel-type id="start-intent-channel" advanced="true">
+               <item-type>String</item-type>
+               <label>Start Intent</label>
+               <description>Start application intent</description>
+       </channel-type>
+
 </thing:thing-descriptions>