[androiddebugbridge] add start intent channel (#12438)

* [androiddebugbridge] add start intent channel

Signed-off-by: Miguel Álvarez Díez <miguelwork92@gmail.com>
This commit is contained in:
GiviMAD 2022-04-30 20:32:11 +02:00 committed by GitHub
parent f94e2af673
commit f4b888a8fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 301 additions and 2 deletions

View 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 | | url | String | Open url in browser |
| media-volume | Dimmer | Set or get media volume level on android device | | media-volume | Dimmer | Set or get media volume level on android device |
| media-control | Player | Control media 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`. | | 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-package | String | Stop application by package name |
| stop-current-package | String | Stop current application | | 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 | | wake-lock | Number | Power wake lock value |
| screen-state | Switch | Screen power state | | 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: #### Available key-event values:
* KEYCODE_0 * KEYCODE_0

View File

@ -33,6 +33,7 @@ public class AndroidDebugBridgeBindingConstants {
public static final ThingTypeUID THING_TYPE_ANDROID_DEVICE = new ThingTypeUID(BINDING_ID, "android"); 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); public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_ANDROID_DEVICE);
// List of all Channel ids // 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 KEY_EVENT_CHANNEL = "key-event";
public static final String TEXT_CHANNEL = "text"; public static final String TEXT_CHANNEL = "text";
public static final String TAP_CHANNEL = "tap"; public static final String TAP_CHANNEL = "tap";

View File

@ -17,6 +17,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket; import java.net.Socket;
import java.net.URI;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -25,6 +26,8 @@ import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@ -65,6 +68,8 @@ public class AndroidDebugBridgeDevice {
private static final Pattern INPUT_EVENT_PATTERN = Pattern private static final Pattern INPUT_EVENT_PATTERN = Pattern
.compile("/(?<input>\\S+): (?<n1>\\S+) (?<n2>\\S+) (?<n3>\\S+)$", Pattern.MULTILINE); .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; private static @Nullable AdbCrypto adbCrypto;
static { static {
@ -170,7 +175,7 @@ public class AndroidDebugBridgeDevice {
logger.warn("{} is not a valid package name", packageName); logger.warn("{} is not a valid package name", packageName);
return; 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); logger.warn("{} is not a valid activity name", activityName);
return; return;
} }
@ -299,7 +304,7 @@ public class AndroidDebugBridgeDevice {
public String getMacAddress() throws AndroidDebugBridgeDeviceException, InterruptedException, public String getMacAddress() throws AndroidDebugBridgeDeviceException, InterruptedException,
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException { 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, 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() { public boolean isConnected() {
var currentSocket = socket; var currentSocket = socket;
return currentSocket != null && currentSocket.isConnected(); return currentSocket != null && currentSocket.isConnected();

View File

@ -180,6 +180,12 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
break; break;
} }
break; break;
case START_INTENT_CHANNEL:
if (command instanceof RefreshType) {
return;
}
adbConnection.startIntent(command.toFullString());
break;
case RECORD_INPUT_CHANNEL: case RECORD_INPUT_CHANNEL:
recordDeviceInput(command); recordDeviceInput(command);
break; break;

View 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.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.label = Port
thing-type.config.androiddebugbridge.android.port.description = Device port listening to adb connections. 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.label = Refresh Time
thing-type.config.androiddebugbridge.android.refreshTime.description = Seconds between device status refreshes. thing-type.config.androiddebugbridge.android.refreshTime.description = Seconds between device status refreshes.
thing-type.config.androiddebugbridge.android.timeout.label = Command Timeout 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.211 = KEYCODE_ZENKAKU_HANKAKU
channel-type.androiddebugbridge.key-event-channel.state.option.168 = KEYCODE_ZOOM_IN 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.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.label = Screen State
channel-type.androiddebugbridge.screen-state-channel.description = Screen Power State channel-type.androiddebugbridge.screen-state-channel.description = Screen Power State
channel-type.androiddebugbridge.shutdown-channel.label = Shutdown channel-type.androiddebugbridge.shutdown-channel.label = Shutdown
channel-type.androiddebugbridge.shutdown-channel.description = Shutdown/Reboot Device 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.POWER_OFF = POWER_OFF
channel-type.androiddebugbridge.shutdown-channel.state.option.REBOOT = REBOOT 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.label = Start Package
channel-type.androiddebugbridge.start-package-channel.description = Run application by package name 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.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.tap-channel.description = Send tap event to android device
channel-type.androiddebugbridge.text-channel.label = Send Text channel-type.androiddebugbridge.text-channel.label = Send Text
channel-type.androiddebugbridge.text-channel.description = Send text to android device 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.label = Wake Lock
channel-type.androiddebugbridge.wake-lock-channel.description = Power Wake Lock State channel-type.androiddebugbridge.wake-lock-channel.description = Power Wake Lock State

View File

@ -24,6 +24,7 @@
<channel id="screen-state" typeId="screen-state-channel"/> <channel id="screen-state" typeId="screen-state-channel"/>
<channel id="shutdown" typeId="shutdown-channel"/> <channel id="shutdown" typeId="shutdown-channel"/>
<channel id="awake-state" typeId="awake-state-channel"/> <channel id="awake-state" typeId="awake-state-channel"/>
<channel id="start-intent" typeId="start-intent-channel"/>
</channels> </channels>
<representation-property>macAddress</representation-property> <representation-property>macAddress</representation-property>
<config-description> <config-description>
@ -447,4 +448,10 @@
<state readOnly="true"/> <state readOnly="true"/>
</channel-type> </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> </thing:thing-descriptions>