[androiddebugbridge] Reconnect on max timeouts and improve volume channel (#15788)

* [androiddebugbridge] Reconnect on max timeouts and improve volume channel

---------

Signed-off-by: Miguel Álvarez <miguelwork92@gmail.com>
This commit is contained in:
GiviMAD 2023-11-03 12:45:48 -07:00 committed by GitHub
parent cb74d85eb0
commit 94d9fb7d36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 113 additions and 55 deletions

View File

@ -10,11 +10,12 @@ If you are not familiar with adb I suggest you to search "How to enable adb over
This binding was tested on :
| Device | Android version | Comments |
|--------------------|-----------------|----------------------------|
| Fire TV Stick | 7.1.2 | Volume control not working |
| Nexus5x | 8.1.0 | Everything works nice |
| Freebox Pop Player | 9 | Everything works nice |
| Device | Android version | Comments |
|------------------------|-----------------|------------------------------------|
| Fire TV Stick | 7.1.2 | Volume control not working |
| Nexus5x | 8.1.0 | Everything works nice |
| Freebox Pop Player | 9 | Everything works nice |
| ChromeCast Google TV | 12 | Volume control partially working |
Please update this document if you tested it with other android versions to reflect the compatibility of the binding.
@ -30,29 +31,31 @@ You could customize the discovery process through the binding options.
## Binding Configuration
| Config | Type | description |
|----------|----------|------------------------------|
| discoveryPort | int | Port used on discovery to connect to the device through adb |
| discoveryReachableMs | int | Milliseconds to wait while discovering to determine if the ip is reachable |
| discoveryIpRangeMin | int | Used to limit the number of IPs checked while discovering |
| discoveryIpRangeMax | int | Used to limit the number of IPs checked while discovering |
| Config | Type | description |
|---------------------|----------|-----------------------------------------------------------------------------------|
| discoveryPort | int | Port used on discovery to connect to the device through adb |
| discoveryReachableMs| int | Milliseconds to wait while discovering to determine if the ip is reachable |
| discoveryIpRangeMin | int | Used to limit the number of IPs checked while discovering |
| discoveryIpRangeMax | int | Used to limit the number of IPs checked while discovering |
## Thing Configuration
| ThingTypeID | description |
|----------|------------------------------|
| android | Android device |
| ThingTypeID | Description |
|---------------|-------------------------|
| android | Android device |
| Config | Type | description |
|----------|----------|------------------------------|
| ip | String | Device ip address |
| port | int | Device port listening to adb connections (default: 5555) |
| refreshTime | int | Seconds between device status refreshes (default: 30) |
| timeout | int | Command timeout in seconds (default: 5) |
| recordDuration | int | Record input duration in seconds |
| deviceMaxVolume | int | Assumed max volume for devices with android versions that do not expose this value. |
| volumeSettingKey | String | Settings key for android versions where volume is gather using settings command (>=android 11). |
| mediaStateJSONConfig | String | Expects a JSON array. Allow to configure the media state detection method per app. Described in the following section |
| Config | Type | Description |
|----------------------|--------|------------------------------------------------------------------------------------------------------------------------|
| ip | String | Device ip address. |
| port | int | Device port listening to adb connections. (default: 5555) |
| refreshTime | int | Seconds between device status refreshes. (default: 30) |
| timeout | int | Command timeout in seconds. (default: 5) |
| recordDuration | int | Record input duration in seconds. |
| deviceMaxVolume | int | Assumed max volume for devices with android versions that do not expose this value. |
| volumeSettingKey | String | Settings key for android versions where volume is gather using settings command. (>=android 11) |
| volumeStepPercent | int | Percent to increase/decrease volume. |
| mediaStateJSONConfig | String | Expects a JSON array. Allow to configure the media state detection method per app. Described in the following section. |
| maxADBTimeouts | int | Max ADB command consecutive timeouts to force to reset the connection. |
## Media State Detection

View File

@ -42,10 +42,18 @@ public class AndroidDebugBridgeConfiguration {
* Record input duration in seconds.
*/
public int recordDuration = 5;
/**
* Percent to increase/decrease volume.
*/
public int volumeStepPercent = 15;
/**
* Assumed max volume for devices with android versions that do not expose this value (>=android 11).
*/
public int deviceMaxVolume = 25;
/**
* Max ADB command consecutive timeouts to force to reset the connection. (0 for disabled)
*/
public int maxADBTimeouts;
/**
* Settings key for android versions where volume is gather using settings command (>=android 11).
*/

View File

@ -13,14 +13,14 @@
package org.openhab.binding.androiddebugbridge.internal;
import java.io.ByteArrayOutputStream;
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;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
@ -56,8 +56,8 @@ import com.tananaev.adblib.AdbStream;
*/
@NonNullByDefault
public class AndroidDebugBridgeDevice {
private static final Path ADB_FOLDER = Path.of(OpenHAB.getUserDataFolder(), ".adb");
public static final int ANDROID_MEDIA_STREAM = 3;
private static final String ADB_FOLDER = OpenHAB.getUserDataFolder() + File.separator + ".adb";
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.*)]");
@ -76,20 +76,6 @@ public class AndroidDebugBridgeDevice {
private static @Nullable AdbCrypto adbCrypto;
static {
var logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class);
try {
File directory = new File(ADB_FOLDER);
if (!directory.exists()) {
directory.mkdir();
}
adbCrypto = loadKeyPair(ADB_FOLDER + File.separator + "adb_pub.key",
ADB_FOLDER + File.separator + "adb.key");
} catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) {
logger.warn("Unable to setup adb keys: {}", e.getMessage());
}
}
private final ScheduledExecutorService scheduler;
private final ReentrantLock commandLock = new ReentrantLock();
@ -793,20 +779,30 @@ public class AndroidDebugBridgeDevice {
}
}
private static AdbBase64 getBase64Impl() {
Charset asciiCharset = Charset.forName("ASCII");
return bytes -> new String(Base64.getEncoder().encode(bytes), asciiCharset);
public static void initADB() {
Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class);
try {
if (!Files.exists(ADB_FOLDER) || !Files.isDirectory(ADB_FOLDER)) {
Files.createDirectory(ADB_FOLDER);
logger.info("Binding folder {} created", ADB_FOLDER);
}
adbCrypto = loadKeyPair(ADB_FOLDER.resolve("adb_pub.key"), ADB_FOLDER.resolve("adb.key"));
} catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) {
logger.warn("Unable to setup adb keys: {}", e.getMessage());
}
}
private static AdbCrypto loadKeyPair(String pubKeyFile, String privKeyFile)
private static AdbBase64 getBase64Impl() {
return bytes -> new String(Base64.getEncoder().encode(bytes), StandardCharsets.US_ASCII);
}
private static AdbCrypto loadKeyPair(Path pubKey, Path privKey)
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
File pub = new File(pubKeyFile);
File priv = new File(privKeyFile);
AdbCrypto c = null;
// load key pair
if (pub.exists() && priv.exists()) {
if (Files.exists(pubKey) && Files.exists(privKey)) {
try {
c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), priv, pub);
c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), privKey.toFile(), pubKey.toFile());
} catch (IOException ignored) {
// Keys don't exits
}
@ -814,7 +810,7 @@ public class AndroidDebugBridgeDevice {
if (c == null) {
// generate key pair
c = AdbCrypto.generateAdbKeyPair(getBase64Impl());
c.saveAdbKeyPair(priv, pub);
c.saveAdbKeyPair(privKey.toFile(), pubKey.toFile());
}
return c;
}

View File

@ -27,6 +27,7 @@ import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
@ -74,6 +75,7 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
private @Nullable ScheduledFuture<?> connectionCheckerSchedule;
private AndroidDebugBridgeMediaStatePackageConfig @Nullable [] packageConfigs = null;
private boolean deviceAwake = false;
private int consecutiveTimeouts = 0;
public AndroidDebugBridgeHandler(Thing thing,
AndroidDebugBridgeDynamicCommandDescriptionProvider commandDescriptionProvider) {
@ -101,6 +103,7 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
logger.warn("{} - read error: {}", currentConfig.ip, e.getMessage());
} catch (TimeoutException e) {
logger.warn("{} - timeout error", currentConfig.ip);
disconnectOnMaxADBTimeouts();
}
}
@ -196,6 +199,7 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
}
break;
}
consecutiveTimeouts = 0;
}
private void recordDeviceInput(Command recordNameCommand)
@ -236,6 +240,16 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
var volumeInfo = adbConnection.getMediaVolume();
maxMediaVolume = volumeInfo.max;
updateState(channelUID, new PercentType((int) Math.round(toPercent(volumeInfo.current, volumeInfo.max))));
} else if (command instanceof IncreaseDecreaseType) {
var volumeInfo = adbConnection.getMediaVolume();
var volumeStep = fromPercent(config.volumeStepPercent, volumeInfo.max);
logger.debug("Device {} volume step: {}", getThing().getUID(), volumeStep);
var targetVolume = (int) Math
.round(IncreaseDecreaseType.INCREASE.equals(command) ? volumeInfo.current + volumeStep
: volumeInfo.current - volumeStep);
var newVolume = Integer.max(0, Integer.min(targetVolume, volumeInfo.max));
logger.debug("Device {} new volume : {}", getThing().getUID(), newVolume);
adbConnection.setMediaVolume(newVolume);
} else {
if (maxMediaVolume == 0) {
return; // We can not transform percentage
@ -250,8 +264,8 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
return (value / maxValue) * 100;
}
private double fromPercent(double value, double maxValue) {
return (value / 100) * maxValue;
private double fromPercent(double percent, double maxValue) {
return (percent / 100) * maxValue;
}
private void handleMediaControlCommand(ChannelUID channelUID, Command command)
@ -398,8 +412,16 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
// Add some information about the device
try {
Map<String, String> editProperties = editProperties();
editProperties.put(Thing.PROPERTY_SERIAL_NUMBER, adbConnection.getSerialNo());
editProperties.put(Thing.PROPERTY_MODEL_ID, adbConnection.getModel());
try {
editProperties.put(Thing.PROPERTY_SERIAL_NUMBER, adbConnection.getSerialNo());
} catch (AndroidDebugBridgeDeviceReadException ignored) {
// Allow devices without serial number.
}
try {
editProperties.put(Thing.PROPERTY_MODEL_ID, adbConnection.getModel());
} catch (AndroidDebugBridgeDeviceReadException ignored) {
// Allow devices without model id.
}
var androidVersion = adbConnection.getAndroidVersion();
editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, androidVersion);
// refresh android version to use
@ -426,8 +448,10 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
} catch (TimeoutException e) {
// happen a lot when device is sleeping; abort refresh other channels
logger.debug("Unable to refresh awake state: Timeout; aborting channels refresh");
disconnectOnMaxADBTimeouts();
return;
}
consecutiveTimeouts = 0;
var awakeStateChannelUID = new ChannelUID(this.thing.getUID(), AWAKE_STATE_CHANNEL);
if (isLinked(awakeStateChannelUID)) {
updateState(awakeStateChannelUID, OnOffType.from(awakeState));
@ -474,6 +498,16 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
}
}
private void disconnectOnMaxADBTimeouts() {
consecutiveTimeouts++;
if (config.maxADBTimeouts > 0 && consecutiveTimeouts >= config.maxADBTimeouts) {
logger.debug("Max consecutive timeouts reached, aborting connection");
adbConnection.disconnect();
checkConnection();
consecutiveTimeouts = 0;
}
}
static class AndroidDebugBridgeMediaStatePackageConfig {
public String name = "";
public @Nullable String label;

View File

@ -41,6 +41,7 @@ public class AndroidDebugBridgeHandlerFactory extends BaseThingHandlerFactory {
public AndroidDebugBridgeHandlerFactory(
final @Reference AndroidDebugBridgeDynamicCommandDescriptionProvider commandDescriptionProvider) {
this.commandDescriptionProvider = commandDescriptionProvider;
AndroidDebugBridgeDevice.initADB();
}
@Override

View File

@ -25,6 +25,8 @@ thing-type.config.androiddebugbridge.android.deviceMaxVolume.label = Device Max
thing-type.config.androiddebugbridge.android.deviceMaxVolume.description = Assumed max volume for devices with android versions that do not expose this value (>=android 11).
thing-type.config.androiddebugbridge.android.ip.label = IP Address
thing-type.config.androiddebugbridge.android.ip.description = Device ip address.
thing-type.config.androiddebugbridge.android.maxADBTimeouts.label = Max ADB Timeouts
thing-type.config.androiddebugbridge.android.maxADBTimeouts.description = Max ADB command consecutive timeouts to force to reset the connection. (0 for disabled)
thing-type.config.androiddebugbridge.android.mediaStateJSONConfig.label = Media State Config
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
@ -45,6 +47,8 @@ thing-type.config.androiddebugbridge.android.volumeSettingKey.option.volume_musi
thing-type.config.androiddebugbridge.android.volumeSettingKey.option.volume_music_headset = volume music headset
thing-type.config.androiddebugbridge.android.volumeSettingKey.option.volume_music_usb_headset = volume music usb headset
thing-type.config.androiddebugbridge.android.volumeSettingKey.option.volume_system = volume system
thing-type.config.androiddebugbridge.android.volumeStepPercent.label = Volume Step Percent
thing-type.config.androiddebugbridge.android.volumeStepPercent.description = Percent to increase/decrease volume.
# channel types

View File

@ -38,7 +38,7 @@
<description>Device port listening to adb connections.</description>
<default>5555</default>
</parameter>
<parameter name="refreshTime" type="integer" min="10" max="120" unit="s" required="true">
<parameter name="refreshTime" type="integer" min="5" max="120" unit="s" required="true">
<label>Refresh Time</label>
<description>Seconds between device status refreshes.</description>
<default>30</default>
@ -75,12 +75,24 @@
</options>
<advanced>true</advanced>
</parameter>
<parameter name="volumeStepPercent" type="integer" min="1" max="100">
<label>Volume Step Percent</label>
<description>Percent to increase/decrease volume.</description>
<default>15</default>
<advanced>true</advanced>
</parameter>
<parameter name="deviceMaxVolume" type="integer" min="1" max="100">
<label>Device Max Volume</label>
<description>Assumed max volume for devices with android versions that do not expose this value (>=android 11).</description>
<default>25</default>
<advanced>true</advanced>
</parameter>
<parameter name="maxADBTimeouts" type="integer" min="0">
<label>Max ADB Timeouts</label>
<description>Max ADB command consecutive timeouts to force to reset the connection. (0 for disabled)</description>
<default>0</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>