Skip to content

Commit

Permalink
[androiddebugbridge] add start intent channel (openhab#12438)
Browse files Browse the repository at this point in the history
* [androiddebugbridge] add start intent channel

Signed-off-by: Miguel Álvarez Díez <miguelwork92@gmail.com>
  • Loading branch information
GiviMAD authored and psmedley committed Feb 23, 2023
1 parent 6d484dd commit 606978d
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 2 deletions.
11 changes: 11 additions & 0 deletions bundles/org.openhab.binding.androiddebugbridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 606978d

Please sign in to comment.