mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
f94e2af673
commit
f4b888a8fd
@ -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
|
||||
|
@ -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";
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
@ -447,4 +448,10 @@
|
||||
<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>
|
||||
|
Loading…
Reference in New Issue
Block a user