[androiddebugbridge] add reboot and tap channels (#10497)

* [androiddebugbridge] avoid concurrent command execution

Signed-off-by: Miguel <miguelwork92@gmail.com>

* [androiddebugbridge] add reboot channel

Signed-off-by: Miguel <miguelwork92@gmail.com>

* [androiddebugbridge] add tap channel

Signed-off-by: Miguel <miguelwork92@gmail.com>

* [androiddebugbridge] validate package name

Signed-off-by: Miguel <miguelwork92@gmail.com>

* [androiddebugbridge] fix reboot channel

Signed-off-by: Miguel <miguelwork92@gmail.com>

* [androiddebugbridge] remove reboot channel and add shutdown channel

Signed-off-by: Miguel <miguelwork92@gmail.com>

* [androiddebugbridge] fix shutdown channel

Signed-off-by: Miguel <miguelwork92@gmail.com>

* [androiddebugbridge] apply spotless

Signed-off-by: Miguel <miguelwork92@gmail.com>
This commit is contained in:
GiviMAD 2021-04-19 19:05:00 +02:00 committed by GitHub
parent fb1d6a283b
commit 5e3717c22a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 22 deletions

View File

@ -58,11 +58,13 @@ This is a sample of the mediaStateJSONConfig thing configuration:
|----------|--------|------------------------------|
| key-event | String | Send key event to android device. Possible values listed below |
| text | String | Send text to android device |
| tap | String | Send tap event to android device (format x,y) |
| media-volume | Dimmer | Set or get media volume level on android device |
| media-control | Player | Control media on android device |
| start-package | String | Run application by package name |
| stop-package | String | Stop application by package name |
| current-package | String | Package name of the top application in screen |
| shutdown | String | Power off/reboot device (allowed values POWER_OFF, REBOOT) |
| wake-lock | Number | Power wake lock value |
| screen-state | Switch | Screen power state |

View File

@ -36,6 +36,7 @@ public class AndroidDebugBridgeBindingConstants {
// List of all Channel ids
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 MEDIA_VOLUME_CHANNEL = "media-volume";
public static final String MEDIA_CONTROL_CHANNEL = "media-control";
public static final String START_PACKAGE_CHANNEL = "start-package";
@ -45,6 +46,8 @@ public class AndroidDebugBridgeBindingConstants {
public static final String AWAKE_STATE_CHANNEL = "awake-state";
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";
// List of all Parameters
public static final String PARAMETER_IP = "ip";
public static final String PARAMETER_PORT = "port";

View File

@ -24,11 +24,8 @@ import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Base64;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -55,6 +52,9 @@ public class AndroidDebugBridgeDevice {
private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class);
private static final Pattern VOLUME_PATTERN = Pattern
.compile("volume is (?<current>\\d.*) in range \\[(?<min>\\d.*)\\.\\.(?<max>\\d.*)]");
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 @Nullable AdbCrypto adbCrypto;
@ -73,6 +73,7 @@ public class AndroidDebugBridgeDevice {
}
private final ScheduledExecutorService scheduler;
private final ReentrantLock commandLock = new ReentrantLock();
private String ip = "127.0.0.1";
private int port = 5555;
@ -101,8 +102,21 @@ public class AndroidDebugBridgeDevice {
runAdbShell("input", "text", URLEncoder.encode(text, StandardCharsets.UTF_8));
}
public void sendTap(String point)
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
var match = TAP_EVENT_PATTERN.matcher(point);
if (!match.matches()) {
throw new AndroidDebugBridgeDeviceException("Unable to parse tap event");
}
runAdbShell("input", "mouse", "tap", match.group("x"), match.group("y"));
}
public void startPackage(String packageName)
throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
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")) {
throw new AndroidDebugBridgeDeviceException("Unable to open package");
@ -111,6 +125,10 @@ public class AndroidDebugBridgeDevice {
public void stopPackage(String packageName)
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
logger.warn("{} is not a valid package name", packageName);
return;
}
runAdbShell("am", "force-stop", packageName);
}
@ -240,6 +258,24 @@ public class AndroidDebugBridgeDevice {
return volumeInfo;
}
public void rebootDevice()
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
try {
runAdbShell("reboot", "&", "sleep", "0.1", "&&", "exit");
} finally {
disconnect();
}
}
public void powerOffDevice()
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
try {
runAdbShell("reboot", "-p", "&", "sleep", "0.1", "&&", "exit");
} finally {
disconnect();
}
}
public boolean isConnected() {
var currentSocket = socket;
return currentSocket != null && currentSocket.isConnected();
@ -281,25 +317,35 @@ public class AndroidDebugBridgeDevice {
if (adb == null) {
throw new AndroidDebugBridgeDeviceException("Device not connected");
}
var commandFuture = scheduler.submit(() -> {
var byteArrayOutputStream = new ByteArrayOutputStream();
String cmd = String.join(" ", args);
logger.debug("{} - shell:{}", ip, cmd);
try {
AdbStream stream = adb.open("shell:" + cmd);
do {
byteArrayOutputStream.writeBytes(stream.read());
} while (!stream.isClosed());
} catch (IOException e) {
String message = e.getMessage();
if (message != null && !message.equals("Stream closed")) {
throw e;
try {
commandLock.lock();
var commandFuture = scheduler.submit(() -> {
var byteArrayOutputStream = new ByteArrayOutputStream();
String cmd = String.join(" ", args);
logger.debug("{} - shell:{}", ip, cmd);
try {
AdbStream stream = adb.open("shell:" + cmd);
do {
byteArrayOutputStream.writeBytes(stream.read());
} while (!stream.isClosed());
} catch (IOException e) {
String message = e.getMessage();
if (message != null && !message.equals("Stream closed")) {
throw e;
}
}
return byteArrayOutputStream.toString(StandardCharsets.US_ASCII);
});
this.commandFuture = commandFuture;
return commandFuture.get(timeoutSec, TimeUnit.SECONDS);
} finally {
var commandFuture = this.commandFuture;
if (commandFuture != null) {
commandFuture.cancel(true);
this.commandFuture = null;
}
return byteArrayOutputStream.toString(StandardCharsets.US_ASCII);
});
this.commandFuture = commandFuture;
return commandFuture.get(timeoutSec, TimeUnit.SECONDS);
commandLock.unlock();
}
}
private static AdbBase64 getBase64Impl() {

View File

@ -58,6 +58,8 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
public static final String KEY_EVENT_PREVIOUS = "88";
public static final String KEY_EVENT_MEDIA_REWIND = "89";
public static final String KEY_EVENT_MEDIA_FAST_FORWARD = "90";
private static final String SHUTDOWN_POWER_OFF = "POWER_OFF";
private static final String SHUTDOWN_REBOOT = "REBOOT";
private static final Gson GSON = new Gson();
private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeHandler.class);
private final AndroidDebugBridgeDevice adbConnection;
@ -111,6 +113,9 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
case TEXT_CHANNEL:
adbConnection.sendText(command.toFullString());
break;
case TAP_CHANNEL:
adbConnection.sendTap(command.toFullString());
break;
case MEDIA_VOLUME_CHANNEL:
handleMediaVolume(channelUID, command);
break;
@ -154,6 +159,17 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
updateState(channelUID, OnOffType.from(screenState));
}
break;
case SHUTDOWN_CHANNEL:
switch (command.toFullString()) {
case SHUTDOWN_POWER_OFF:
adbConnection.powerOffDevice();
updateStatus(ThingStatus.OFFLINE);
break;
case SHUTDOWN_REBOOT:
adbConnection.rebootDevice();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Rebooting");
break;
}
}
}

View File

@ -10,6 +10,7 @@
<channels>
<channel id="key-event" typeId="key-event-channel"/>
<channel id="text" typeId="text-channel"/>
<channel id="tap" typeId="tap-channel"/>
<channel id="media-volume" typeId="system.volume"/>
<channel id="media-control" typeId="system.media-control"/>
<channel id="start-package" typeId="start-package-channel"/>
@ -18,6 +19,7 @@
<channel id="current-package" typeId="current-package-channel"/>
<channel id="wake-lock" typeId="wake-lock-channel"/>
<channel id="screen-state" typeId="screen-state-channel"/>
<channel id="shutdown" typeId="shutdown-channel"/>
<channel id="awake-state" typeId="awake-state-channel"/>
</channels>
<representation-property>serial</representation-property>
@ -355,6 +357,12 @@
<description>Send text to android device</description>
</channel-type>
<channel-type id="tap-channel">
<item-type>String</item-type>
<label>Send Tap</label>
<description>Send tap event to android device</description>
</channel-type>
<channel-type id="start-package-channel">
<item-type>String</item-type>
<label>Start Package</label>
@ -380,6 +388,18 @@
<state readOnly="true"/>
</channel-type>
<channel-type id="shutdown-channel" advanced="true">
<item-type>String</item-type>
<label>Shutdown</label>
<description>Shutdown/Reboot Device</description>
<state>
<options>
<option value="POWER_OFF">POWER_OFF</option>
<option value="REBOOT">REBOOT</option>
</options>
</state>
</channel-type>
<channel-type id="wake-lock-channel" advanced="true">
<item-type>Number</item-type>
<label>Wake Lock</label>