[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 : This binding was tested on :
| Device | Android version | Comments | | Device | Android version | Comments |
|--------------------|-----------------|----------------------------| |------------------------|-----------------|------------------------------------|
| Fire TV Stick | 7.1.2 | Volume control not working | | Fire TV Stick | 7.1.2 | Volume control not working |
| Nexus5x | 8.1.0 | Everything works nice | | Nexus5x | 8.1.0 | Everything works nice |
| Freebox Pop Player | 9 | 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. 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 ## Binding Configuration
| Config | Type | description | | Config | Type | description |
|----------|----------|------------------------------| |---------------------|----------|-----------------------------------------------------------------------------------|
| discoveryPort | int | Port used on discovery to connect to the device through adb | | 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 | | 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 | | discoveryIpRangeMin | int | Used to limit the number of IPs checked while discovering |
| discoveryIpRangeMax | 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 ## Thing Configuration
| ThingTypeID | description | | ThingTypeID | Description |
|----------|------------------------------| |---------------|-------------------------|
| android | Android device | | android | Android device |
| Config | Type | description | | Config | Type | Description |
|----------|----------|------------------------------| |----------------------|--------|------------------------------------------------------------------------------------------------------------------------|
| ip | String | Device ip address | | ip | String | Device ip address. |
| port | int | Device port listening to adb connections (default: 5555) | | port | int | Device port listening to adb connections. (default: 5555) |
| refreshTime | int | Seconds between device status refreshes (default: 30) | | refreshTime | int | Seconds between device status refreshes. (default: 30) |
| timeout | int | Command timeout in seconds (default: 5) | | timeout | int | Command timeout in seconds. (default: 5) |
| recordDuration | int | Record input duration in seconds | | recordDuration | int | Record input duration in seconds. |
| deviceMaxVolume | int | Assumed max volume for devices with android versions that do not expose this value. | | 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). | | 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 | | 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 ## Media State Detection

View File

@ -42,10 +42,18 @@ public class AndroidDebugBridgeConfiguration {
* Record input duration in seconds. * Record input duration in seconds.
*/ */
public int recordDuration = 5; 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). * Assumed max volume for devices with android versions that do not expose this value (>=android 11).
*/ */
public int deviceMaxVolume = 25; 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). * 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; package org.openhab.binding.androiddebugbridge.internal;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket; import java.net.Socket;
import java.net.URI; import java.net.URI;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList; import java.util.ArrayList;
@ -56,8 +56,8 @@ import com.tananaev.adblib.AdbStream;
*/ */
@NonNullByDefault @NonNullByDefault
public class AndroidDebugBridgeDevice { public class AndroidDebugBridgeDevice {
private static final Path ADB_FOLDER = Path.of(OpenHAB.getUserDataFolder(), ".adb");
public static final int ANDROID_MEDIA_STREAM = 3; 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 final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class);
private static final Pattern VOLUME_PATTERN = Pattern private static final Pattern VOLUME_PATTERN = Pattern
.compile("volume is (?<current>\\d.*) in range \\[(?<min>\\d.*)\\.\\.(?<max>\\d.*)]"); .compile("volume is (?<current>\\d.*) in range \\[(?<min>\\d.*)\\.\\.(?<max>\\d.*)]");
@ -76,20 +76,6 @@ public class AndroidDebugBridgeDevice {
private static @Nullable AdbCrypto adbCrypto; 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 ScheduledExecutorService scheduler;
private final ReentrantLock commandLock = new ReentrantLock(); private final ReentrantLock commandLock = new ReentrantLock();
@ -793,20 +779,30 @@ public class AndroidDebugBridgeDevice {
} }
} }
private static AdbBase64 getBase64Impl() { public static void initADB() {
Charset asciiCharset = Charset.forName("ASCII"); Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class);
return bytes -> new String(Base64.getEncoder().encode(bytes), asciiCharset); 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 { throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
File pub = new File(pubKeyFile);
File priv = new File(privKeyFile);
AdbCrypto c = null; AdbCrypto c = null;
// load key pair // load key pair
if (pub.exists() && priv.exists()) { if (Files.exists(pubKey) && Files.exists(privKey)) {
try { try {
c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), priv, pub); c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), privKey.toFile(), pubKey.toFile());
} catch (IOException ignored) { } catch (IOException ignored) {
// Keys don't exits // Keys don't exits
} }
@ -814,7 +810,7 @@ public class AndroidDebugBridgeDevice {
if (c == null) { if (c == null) {
// generate key pair // generate key pair
c = AdbCrypto.generateAdbKeyPair(getBase64Impl()); c = AdbCrypto.generateAdbKeyPair(getBase64Impl());
c.saveAdbKeyPair(priv, pub); c.saveAdbKeyPair(privKey.toFile(), pubKey.toFile());
} }
return c; return c;
} }

View File

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

View File

@ -41,6 +41,7 @@ public class AndroidDebugBridgeHandlerFactory extends BaseThingHandlerFactory {
public AndroidDebugBridgeHandlerFactory( public AndroidDebugBridgeHandlerFactory(
final @Reference AndroidDebugBridgeDynamicCommandDescriptionProvider commandDescriptionProvider) { final @Reference AndroidDebugBridgeDynamicCommandDescriptionProvider commandDescriptionProvider) {
this.commandDescriptionProvider = commandDescriptionProvider; this.commandDescriptionProvider = commandDescriptionProvider;
AndroidDebugBridgeDevice.initADB();
} }
@Override @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.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.label = IP Address
thing-type.config.androiddebugbridge.android.ip.description = Device 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.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.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.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_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_music_usb_headset = volume music usb headset
thing-type.config.androiddebugbridge.android.volumeSettingKey.option.volume_system = volume system 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 # channel types

View File

@ -38,7 +38,7 @@
<description>Device port listening to adb connections.</description> <description>Device port listening to adb connections.</description>
<default>5555</default> <default>5555</default>
</parameter> </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> <label>Refresh Time</label>
<description>Seconds between device status refreshes.</description> <description>Seconds between device status refreshes.</description>
<default>30</default> <default>30</default>
@ -75,12 +75,24 @@
</options> </options>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </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"> <parameter name="deviceMaxVolume" type="integer" min="1" max="100">
<label>Device Max Volume</label> <label>Device Max Volume</label>
<description>Assumed max volume for devices with android versions that do not expose this value (>=android 11).</description> <description>Assumed max volume for devices with android versions that do not expose this value (>=android 11).</description>
<default>25</default> <default>25</default>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </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> </config-description>
</thing-type> </thing-type>