[androiddebugbridge] Add channels for record events, open urls and doc improvements (#11692)

* Add channels for record events, open urls and doc improvements

Signed-off-by: Miguel Álvarez Díez <miguelwork92@gmail.com>
This commit is contained in:
GiviMAD
2021-12-20 19:21:11 +01:00
committed by GitHub
parent dd951cee02
commit 208885deb6
7 changed files with 208 additions and 10 deletions

View File

@@ -37,6 +37,7 @@ public class AndroidDebugBridgeBindingConstants {
public static final String KEY_EVENT_CHANNEL = "key-event";
public static final String TEXT_CHANNEL = "text";
public static final String TAP_CHANNEL = "tap";
public static final String URL_CHANNEL = "url";
public static final String MEDIA_VOLUME_CHANNEL = "media-volume";
public static final String MEDIA_CONTROL_CHANNEL = "media-control";
public static final String START_PACKAGE_CHANNEL = "start-package";
@@ -47,7 +48,8 @@ public class AndroidDebugBridgeBindingConstants {
public static final String WAKE_LOCK_CHANNEL = "wake-lock";
public static final String SCREEN_STATE_CHANNEL = "screen-state";
public static final String SHUTDOWN_CHANNEL = "shutdown";
public static final String RECORD_INPUT_CHANNEL = "record-input";
public static final String RECORDED_INPUT_CHANNEL = "recorded-input";
// List of all Parameters
public static final String PARAMETER_IP = "ip";
public static final String PARAMETER_PORT = "port";

View File

@@ -38,6 +38,10 @@ public class AndroidDebugBridgeConfiguration {
* Command timeout seconds.
*/
public int timeout = 5;
/**
* Record input duration in seconds.
*/
public int recordDuration = 5;
/**
* Configure media state detection behavior by package
*/

View File

@@ -22,6 +22,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.concurrent.*;
@@ -55,6 +56,10 @@ public class AndroidDebugBridgeDevice {
private static final Pattern TAP_EVENT_PATTERN = Pattern.compile("(?<x>\\d+),(?<y>\\d+)");
private static final Pattern PACKAGE_NAME_PATTERN = Pattern
.compile("^([A-Za-z]{1}[A-Za-z\\d_]*\\.)+[A-Za-z][A-Za-z\\d_]*$");
private static final Pattern URL_PATTERN = Pattern.compile(
"https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)$");
private static final Pattern INPUT_EVENT_PATTERN = Pattern
.compile("/(?<input>\\S+): (?<n1>\\S+) (?<n2>\\S+) (?<n3>\\S+)$", Pattern.MULTILINE);
private static @Nullable AdbCrypto adbCrypto;
@@ -78,6 +83,7 @@ public class AndroidDebugBridgeDevice {
private String ip = "127.0.0.1";
private int port = 5555;
private int timeoutSec = 5;
private int recordDuration;
private @Nullable Socket socket;
private @Nullable AdbConnection connection;
private @Nullable Future<String> commandFuture;
@@ -86,10 +92,11 @@ public class AndroidDebugBridgeDevice {
this.scheduler = scheduler;
}
public void configure(String ip, int port, int timeout) {
public void configure(String ip, int port, int timeout, int recordDuration) {
this.ip = ip;
this.port = port;
this.timeoutSec = timeout;
this.recordDuration = recordDuration;
}
public void sendKeyEvent(String eventCode)
@@ -111,18 +118,68 @@ public class AndroidDebugBridgeDevice {
runAdbShell("input", "mouse", "tap", match.group("x"), match.group("y"));
}
public void openUrl(String url)
throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
var match = URL_PATTERN.matcher(url);
if (!match.matches()) {
throw new AndroidDebugBridgeDeviceException("Unable to parse url");
}
runAdbShell("am", "start", "-a", url);
}
public void startPackage(String packageName)
throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
if (packageName.contains("/")) {
startPackageWithActivity(packageName);
return;
}
if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
logger.warn("{} is not a valid package name", packageName);
return;
}
var out = runAdbShell("monkey", "--pct-syskeys", "0", "-p", packageName, "-v", "1");
if (out.contains("monkey aborted")) {
startTVPackage(packageName);
}
}
private void startTVPackage(String packageName)
throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
// https://developer.android.com/training/tv/start/start
String result = runAdbShell("monkey", "--pct-syskeys", "0", "-c", "android.intent.category.LEANBACK_LAUNCHER",
"-p", packageName, "1");
if (result.contains("monkey aborted")) {
throw new AndroidDebugBridgeDeviceException("Unable to open package");
}
}
public void startPackageWithActivity(String packageWithActivity)
throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
var parts = packageWithActivity.split("/");
if (parts.length != 2) {
logger.warn("{} is not a valid package", packageWithActivity);
return;
}
var packageName = parts[0];
var activityName = parts[1];
if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
logger.warn("{} is not a valid package name", packageName);
return;
}
if (!PACKAGE_NAME_PATTERN.matcher(activityName).matches()) {
logger.warn("{} is not a valid activity name", activityName);
return;
}
var out = runAdbShell("am", "start", "-n", packageWithActivity);
if (out.contains("usage: am")) {
out = runAdbShell("am", "start", packageWithActivity);
}
if (out.contains("usage: am") || out.contains("Exception")) {
logger.warn("open {} fail; retrying to open without activity info", packageWithActivity);
startPackage(packageName);
}
}
public void stopPackage(String packageName)
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
@@ -160,7 +217,7 @@ public class AndroidDebugBridgeDevice {
var state = devicesResp.split("=")[1].trim();
return state.equals("ON");
} catch (NumberFormatException e) {
logger.debug("Unable to parse device wake lock: {}", e.getMessage());
logger.debug("Unable to parse device screen state: {}", e.getMessage());
}
}
throw new AndroidDebugBridgeDeviceReadException("Unable to read screen state");
@@ -258,6 +315,36 @@ public class AndroidDebugBridgeDevice {
return volumeInfo;
}
public String recordInputEvents()
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
String out = runAdbShell(recordDuration * 2, "getevent", "&", "sleep", Integer.toString(recordDuration), "&&",
"exit");
var matcher = INPUT_EVENT_PATTERN.matcher(out);
var commandList = new ArrayList<String>();
try {
while (matcher.find()) {
String inputPath = matcher.group("input");
int n1 = Integer.parseInt(matcher.group("n1"), 16);
int n2 = Integer.parseInt(matcher.group("n2"), 16);
int n3 = Integer.parseInt(matcher.group("n3"), 16);
commandList.add(String.format("sendevent /%s %d %d %d", inputPath, n1, n2, n3));
}
} catch (NumberFormatException e) {
logger.warn("NumberFormatException while parsing events, aborting");
return "";
}
return String.join(" && ", commandList);
}
public void sendInputEvents(String command)
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
String out = runAdbShell(command.split(" "));
if (out.length() != 0) {
logger.warn("Device event unexpected output: {}", out);
throw new AndroidDebugBridgeDeviceException("Device event execution fail");
}
}
public void rebootDevice()
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
try {
@@ -313,6 +400,11 @@ public class AndroidDebugBridgeDevice {
private String runAdbShell(String... args)
throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
return runAdbShell(timeoutSec, args);
}
private String runAdbShell(int commandTimeout, String... args)
throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
var adb = connection;
if (adb == null) {
throw new AndroidDebugBridgeDeviceException("Device not connected");
@@ -337,7 +429,7 @@ public class AndroidDebugBridgeDevice {
return byteArrayOutputStream.toString(StandardCharsets.US_ASCII);
});
this.commandFuture = commandFuture;
return commandFuture.get(timeoutSec, TimeUnit.SECONDS);
return commandFuture.get(commandTimeout, TimeUnit.SECONDS);
} finally {
var commandFuture = this.commandFuture;
if (commandFuture != null) {

View File

@@ -129,7 +129,7 @@ public class AndroidDebugBridgeDiscoveryService extends AbstractDiscoveryService
private void discoverWithADB(String ip, int port) throws InterruptedException, AndroidDebugBridgeDeviceException,
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
var device = new AndroidDebugBridgeDevice(scheduler);
device.configure(ip, port, 10);
device.configure(ip, port, 10, 0);
try {
device.connect();
logger.debug("connected adb at {}:{}", ip, port);

View File

@@ -20,6 +20,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -61,6 +62,7 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
private static final String SHUTDOWN_POWER_OFF = "POWER_OFF";
private static final String SHUTDOWN_REBOOT = "REBOOT";
private static final Gson GSON = new Gson();
private static final Pattern RECORD_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]*$");
private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeHandler.class);
private final AndroidDebugBridgeDevice adbConnection;
private int maxMediaVolume = 0;
@@ -116,6 +118,9 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
case TAP_CHANNEL:
adbConnection.sendTap(command.toFullString());
break;
case URL_CHANNEL:
adbConnection.openUrl(command.toFullString());
break;
case MEDIA_VOLUME_CHANNEL:
handleMediaVolume(channelUID, command);
break;
@@ -170,9 +175,51 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Rebooting");
break;
}
break;
case RECORD_INPUT_CHANNEL:
recordDeviceInput(command);
break;
case RECORDED_INPUT_CHANNEL:
String recordName = getRecordPropertyName(command);
var inputCommand = this.getThing().getProperties().get(recordName);
if (inputCommand != null) {
adbConnection.sendInputEvents(inputCommand);
}
break;
}
}
private void recordDeviceInput(Command recordNameCommand)
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
var recordName = recordNameCommand.toFullString();
if (!RECORD_NAME_PATTERN.matcher(recordName).matches()) {
logger.warn("Invalid record name, accepts alphanumeric values with '_'.");
return;
}
String recordPropertyName = getRecordPropertyName(recordName);
logger.debug("RECORD: {}", recordPropertyName);
var eventCommand = adbConnection.recordInputEvents();
if (eventCommand.isEmpty()) {
logger.debug("No events recorded");
if (this.getThing().getProperties().containsKey(recordPropertyName)) {
this.getThing().setProperty(recordPropertyName, null);
updateProperties(editProperties());
logger.debug("Record {} deleted", recordName);
}
} else {
updateProperty(recordPropertyName, eventCommand);
logger.debug("New record {}: {}", recordName, eventCommand);
}
}
private String getRecordPropertyName(String recordName) {
return String.format("input-record:%s", recordName);
}
private String getRecordPropertyName(Command recordNameCommand) {
return getRecordPropertyName(recordNameCommand.toFullString());
}
private void handleMediaVolume(ChannelUID channelUID, Command command)
throws InterruptedException, AndroidDebugBridgeDeviceReadException, AndroidDebugBridgeDeviceException,
TimeoutException, ExecutionException {
@@ -264,7 +311,8 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
if (mediaStateJSONConfig != null && !mediaStateJSONConfig.isEmpty()) {
loadMediaStateConfig(mediaStateJSONConfig);
}
adbConnection.configure(currentConfig.ip, currentConfig.port, currentConfig.timeout);
adbConnection.configure(currentConfig.ip, currentConfig.port, currentConfig.timeout,
currentConfig.recordDuration);
updateStatus(ThingStatus.UNKNOWN);
connectionCheckerSchedule = scheduler.scheduleWithFixedDelay(this::checkConnection, 0,
currentConfig.refreshTime, TimeUnit.SECONDS);
@@ -331,7 +379,8 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
awakeState = adbConnection.isAwake();
deviceAwake = awakeState;
} catch (TimeoutException e) {
logger.warn("Unable to refresh awake state: Timeout");
// happen a lot when device is sleeping; abort refresh other channels
logger.debug("Unable to refresh awake state: Timeout; aborting channels refresh");
return;
}
var awakeStateChannelUID = new ChannelUID(this.thing.getUID(), AWAKE_STATE_CHANNEL);
@@ -339,6 +388,7 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
updateState(awakeStateChannelUID, OnOffType.from(awakeState));
}
if (!awakeState && !prevDeviceAwake) {
// abort refresh channels while device is sleeping, throws many timeouts
logger.debug("device {} is sleeping", config.ip);
return;
}

View File

@@ -11,6 +11,9 @@
<channel id="key-event" typeId="key-event-channel"/>
<channel id="text" typeId="text-channel"/>
<channel id="tap" typeId="tap-channel"/>
<channel id="url" typeId="url-channel"/>
<channel id="record-input" typeId="record-input-channel"/>
<channel id="recorded-input" typeId="recorded-input-channel"/>
<channel id="media-volume" typeId="system.volume"/>
<channel id="media-control" typeId="system.media-control"/>
<channel id="start-package" typeId="start-package-channel"/>
@@ -44,6 +47,11 @@
<description>Command timeout seconds.</description>
<default>5</default>
</parameter>
<parameter name="recordDuration" type="integer" required="true" min="3" max="20">
<label>Record Duration</label>
<description>How much time the record-input channel wait for events to record.</description>
<default>5</default>
</parameter>
<parameter name="mediaStateJSONConfig" type="text">
<label>Media State Config</label>
<description>JSON config that allows to modify the media state detection strategy for each app. Refer to the binding
@@ -363,6 +371,24 @@
<description>Send tap event to android device</description>
</channel-type>
<channel-type id="url-channel">
<item-type>String</item-type>
<label>Open Url</label>
<description>Open url in the browser</description>
</channel-type>
<channel-type id="record-input-channel">
<item-type>String</item-type>
<label>Record Input</label>
<description>Record input events under provided name</description>
</channel-type>
<channel-type id="recorded-input-channel">
<item-type>String</item-type>
<label>Recorded Input</label>
<description>Send previous recorded input events by name</description>
</channel-type>
<channel-type id="start-package-channel">
<item-type>String</item-type>
<label>Start Package</label>