2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.androiddebugbridge.internal;
15 import static org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeBindingConstants.*;
17 import java.util.Arrays;
18 import java.util.List;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24 import java.util.regex.Pattern;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.core.library.types.DecimalType;
29 import org.openhab.core.library.types.NextPreviousType;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.PercentType;
32 import org.openhab.core.library.types.PlayPauseType;
33 import org.openhab.core.library.types.RewindFastforwardType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.binding.BaseThingHandler;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
45 import com.google.gson.Gson;
46 import com.google.gson.JsonSyntaxException;
49 * The {@link AndroidDebugBridgeHandler} is responsible for handling commands, which are
50 * sent to one of the channels.
52 * @author Miguel Álvarez - Initial contribution
55 public class AndroidDebugBridgeHandler extends BaseThingHandler {
57 public static final String KEY_EVENT_PLAY = "126";
58 public static final String KEY_EVENT_PAUSE = "127";
59 public static final String KEY_EVENT_NEXT = "87";
60 public static final String KEY_EVENT_PREVIOUS = "88";
61 public static final String KEY_EVENT_MEDIA_REWIND = "89";
62 public static final String KEY_EVENT_MEDIA_FAST_FORWARD = "90";
63 private static final String SHUTDOWN_POWER_OFF = "POWER_OFF";
64 private static final String SHUTDOWN_REBOOT = "REBOOT";
65 private static final Gson GSON = new Gson();
66 private static final Pattern RECORD_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]*$");
67 private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeHandler.class);
68 private final AndroidDebugBridgeDevice adbConnection;
69 private int maxMediaVolume = 0;
70 private AndroidDebugBridgeConfiguration config = new AndroidDebugBridgeConfiguration();
71 private @Nullable ScheduledFuture<?> connectionCheckerSchedule;
72 private AndroidDebugBridgeMediaStatePackageConfig @Nullable [] packageConfigs = null;
73 private boolean deviceAwake = false;
75 public AndroidDebugBridgeHandler(Thing thing) {
77 this.adbConnection = new AndroidDebugBridgeDevice(scheduler);
81 public void handleCommand(ChannelUID channelUID, Command command) {
82 AndroidDebugBridgeConfiguration currentConfig = config;
84 if (!adbConnection.isConnected()) {
86 adbConnection.connect();
88 handleCommandInternal(channelUID, command);
89 } catch (InterruptedException ignored) {
90 } catch (AndroidDebugBridgeDeviceException | ExecutionException e) {
91 if (!(e.getCause() instanceof InterruptedException)) {
92 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
93 adbConnection.disconnect();
95 } catch (AndroidDebugBridgeDeviceReadException e) {
96 logger.warn("{} - read error: {}", currentConfig.ip, e.getMessage());
97 } catch (TimeoutException e) {
98 logger.warn("{} - timeout error", currentConfig.ip);
102 private void handleCommandInternal(ChannelUID channelUID, Command command)
103 throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
104 TimeoutException, ExecutionException {
105 if (!isLinked(channelUID)) {
108 String channelId = channelUID.getId();
110 case KEY_EVENT_CHANNEL:
111 adbConnection.sendKeyEvent(command.toFullString());
114 adbConnection.sendText(command.toFullString());
117 adbConnection.sendTap(command.toFullString());
120 adbConnection.openUrl(command.toFullString());
122 case MEDIA_VOLUME_CHANNEL:
123 handleMediaVolume(channelUID, command);
125 case MEDIA_CONTROL_CHANNEL:
126 handleMediaControlCommand(channelUID, command);
128 case START_PACKAGE_CHANNEL:
129 adbConnection.startPackage(command.toFullString());
130 updateState(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL),
131 new StringType(command.toFullString()));
133 case STOP_PACKAGE_CHANNEL:
134 adbConnection.stopPackage(command.toFullString());
136 case STOP_CURRENT_PACKAGE_CHANNEL:
137 if (OnOffType.from(command.toFullString()).equals(OnOffType.OFF)) {
138 adbConnection.stopPackage(adbConnection.getCurrentPackage());
141 case CURRENT_PACKAGE_CHANNEL:
142 if (command instanceof RefreshType) {
143 var packageName = adbConnection.getCurrentPackage();
144 updateState(channelUID, new StringType(packageName));
147 case WAKE_LOCK_CHANNEL:
148 if (command instanceof RefreshType) {
149 int lock = adbConnection.getPowerWakeLock();
150 updateState(channelUID, new DecimalType(lock));
153 case AWAKE_STATE_CHANNEL:
154 if (command instanceof RefreshType) {
155 boolean awakeState = adbConnection.isAwake();
156 updateState(channelUID, OnOffType.from(awakeState));
159 case SCREEN_STATE_CHANNEL:
160 if (command instanceof RefreshType) {
161 boolean screenState = adbConnection.isScreenOn();
162 updateState(channelUID, OnOffType.from(screenState));
165 case SHUTDOWN_CHANNEL:
166 switch (command.toFullString()) {
167 case SHUTDOWN_POWER_OFF:
168 adbConnection.powerOffDevice();
169 updateStatus(ThingStatus.OFFLINE);
171 case SHUTDOWN_REBOOT:
172 adbConnection.rebootDevice();
173 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Rebooting");
177 case RECORD_INPUT_CHANNEL:
178 recordDeviceInput(command);
180 case RECORDED_INPUT_CHANNEL:
181 String recordName = getRecordPropertyName(command);
182 var inputCommand = this.getThing().getProperties().get(recordName);
183 if (inputCommand != null) {
184 adbConnection.sendInputEvents(inputCommand);
190 private void recordDeviceInput(Command recordNameCommand)
191 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
192 var recordName = recordNameCommand.toFullString();
193 if (!RECORD_NAME_PATTERN.matcher(recordName).matches()) {
194 logger.warn("Invalid record name, accepts alphanumeric values with '_'.");
197 String recordPropertyName = getRecordPropertyName(recordName);
198 logger.debug("RECORD: {}", recordPropertyName);
199 var eventCommand = adbConnection.recordInputEvents();
200 if (eventCommand.isEmpty()) {
201 logger.debug("No events recorded");
202 if (this.getThing().getProperties().containsKey(recordPropertyName)) {
203 this.getThing().setProperty(recordPropertyName, null);
204 updateProperties(editProperties());
205 logger.debug("Record {} deleted", recordName);
208 updateProperty(recordPropertyName, eventCommand);
209 logger.debug("New record {}: {}", recordName, eventCommand);
213 private String getRecordPropertyName(String recordName) {
214 return String.format("input-record:%s", recordName);
217 private String getRecordPropertyName(Command recordNameCommand) {
218 return getRecordPropertyName(recordNameCommand.toFullString());
221 private void handleMediaVolume(ChannelUID channelUID, Command command)
222 throws InterruptedException, AndroidDebugBridgeDeviceReadException, AndroidDebugBridgeDeviceException,
223 TimeoutException, ExecutionException {
224 if (command instanceof RefreshType) {
225 var volumeInfo = adbConnection.getMediaVolume();
226 maxMediaVolume = volumeInfo.max;
227 updateState(channelUID, new PercentType((int) Math.round(toPercent(volumeInfo.current, volumeInfo.max))));
229 if (maxMediaVolume == 0) {
230 return; // We can not transform percentage
232 int targetVolume = Integer.parseInt(command.toFullString());
233 adbConnection.setMediaVolume((int) Math.round(fromPercent(targetVolume, maxMediaVolume)));
234 updateState(channelUID, new PercentType(targetVolume));
238 private double toPercent(double value, double maxValue) {
239 return (value / maxValue) * 100;
242 private double fromPercent(double value, double maxValue) {
243 return (value / 100) * maxValue;
246 private void handleMediaControlCommand(ChannelUID channelUID, Command command)
247 throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
248 TimeoutException, ExecutionException {
249 if (command instanceof RefreshType) {
251 String currentPackage = adbConnection.getCurrentPackage();
252 var currentPackageConfig = packageConfigs != null ? Arrays.stream(packageConfigs)
253 .filter(pc -> pc.name.equals(currentPackage)).findFirst().orElse(null) : null;
254 if (currentPackageConfig != null) {
255 logger.debug("media stream config found for {}, mode: {}", currentPackage, currentPackageConfig.mode);
256 switch (currentPackageConfig.mode) {
261 int wakeLockState = adbConnection.getPowerWakeLock();
262 playing = currentPackageConfig.wakeLockPlayStates.contains(wakeLockState);
265 playing = adbConnection.isPlayingMedia(currentPackage);
268 playing = adbConnection.isPlayingAudio();
271 logger.warn("media state config: package {} unsupported mode", currentPackage);
275 logger.debug("media stream config not found for {}", currentPackage);
276 playing = adbConnection.isPlayingMedia(currentPackage);
278 updateState(channelUID, playing ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
279 } else if (command instanceof PlayPauseType) {
280 if (command == PlayPauseType.PLAY) {
281 adbConnection.sendKeyEvent(KEY_EVENT_PLAY);
282 updateState(channelUID, PlayPauseType.PLAY);
283 } else if (command == PlayPauseType.PAUSE) {
284 adbConnection.sendKeyEvent(KEY_EVENT_PAUSE);
285 updateState(channelUID, PlayPauseType.PAUSE);
287 } else if (command instanceof NextPreviousType) {
288 if (command == NextPreviousType.NEXT) {
289 adbConnection.sendKeyEvent(KEY_EVENT_NEXT);
290 } else if (command == NextPreviousType.PREVIOUS) {
291 adbConnection.sendKeyEvent(KEY_EVENT_PREVIOUS);
293 } else if (command instanceof RewindFastforwardType) {
294 if (command == RewindFastforwardType.FASTFORWARD) {
295 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_FAST_FORWARD);
296 } else if (command == RewindFastforwardType.REWIND) {
297 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_REWIND);
300 logger.warn("Unknown media control command: {}", command);
305 public void initialize() {
306 AndroidDebugBridgeConfiguration currentConfig = getConfigAs(AndroidDebugBridgeConfiguration.class);
307 config = currentConfig;
308 var mediaStateJSONConfig = currentConfig.mediaStateJSONConfig;
309 if (mediaStateJSONConfig != null && !mediaStateJSONConfig.isEmpty()) {
310 loadMediaStateConfig(mediaStateJSONConfig);
312 adbConnection.configure(currentConfig.ip, currentConfig.port, currentConfig.timeout,
313 currentConfig.recordDuration);
314 updateStatus(ThingStatus.UNKNOWN);
315 connectionCheckerSchedule = scheduler.scheduleWithFixedDelay(this::checkConnection, 0,
316 currentConfig.refreshTime, TimeUnit.SECONDS);
319 private void loadMediaStateConfig(String mediaStateJSONConfig) {
321 this.packageConfigs = GSON.fromJson(mediaStateJSONConfig,
322 AndroidDebugBridgeMediaStatePackageConfig[].class);
323 } catch (JsonSyntaxException e) {
324 logger.warn("unable to parse media state config: {}", e.getMessage());
329 public void dispose() {
330 var schedule = connectionCheckerSchedule;
331 if (schedule != null) {
332 schedule.cancel(true);
333 connectionCheckerSchedule = null;
335 packageConfigs = null;
336 adbConnection.disconnect();
340 public void checkConnection() {
341 AndroidDebugBridgeConfiguration currentConfig = config;
343 logger.debug("Refresh device {} status", currentConfig.ip);
344 if (adbConnection.isConnected()) {
345 updateStatus(ThingStatus.ONLINE);
350 adbConnection.connect();
351 } catch (AndroidDebugBridgeDeviceException e) {
352 logger.debug("Error connecting to device; [{}]: {}", e.getClass().getCanonicalName(),
354 adbConnection.disconnect();
355 updateStatus(ThingStatus.OFFLINE);
358 if (adbConnection.isConnected()) {
359 updateStatus(ThingStatus.ONLINE);
364 } catch (InterruptedException ignored) {
365 } catch (AndroidDebugBridgeDeviceException | AndroidDebugBridgeDeviceReadException | ExecutionException e) {
366 logger.debug("Connection checker error: {}", e.getMessage());
367 adbConnection.disconnect();
368 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
372 private void refreshProperties() throws InterruptedException, AndroidDebugBridgeDeviceException,
373 AndroidDebugBridgeDeviceReadException, ExecutionException {
374 // Add some information about the device
376 Map<String, String> editProperties = editProperties();
377 editProperties.put(Thing.PROPERTY_SERIAL_NUMBER, adbConnection.getSerialNo());
378 editProperties.put(Thing.PROPERTY_MODEL_ID, adbConnection.getModel());
379 editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, adbConnection.getAndroidVersion());
380 editProperties.put(Thing.PROPERTY_VENDOR, adbConnection.getBrand());
382 editProperties.put(Thing.PROPERTY_MAC_ADDRESS, adbConnection.getMacAddress());
383 } catch (AndroidDebugBridgeDeviceReadException e) {
384 logger.debug("Refresh properties error: {}", e.getMessage());
386 updateProperties(editProperties);
387 } catch (TimeoutException e) {
388 logger.debug("Refresh properties error: Timeout");
393 private void refreshStatus() throws InterruptedException, AndroidDebugBridgeDeviceException, ExecutionException {
395 boolean prevDeviceAwake = deviceAwake;
397 awakeState = adbConnection.isAwake();
398 deviceAwake = awakeState;
399 } catch (TimeoutException e) {
400 // happen a lot when device is sleeping; abort refresh other channels
401 logger.debug("Unable to refresh awake state: Timeout; aborting channels refresh");
404 var awakeStateChannelUID = new ChannelUID(this.thing.getUID(), AWAKE_STATE_CHANNEL);
405 if (isLinked(awakeStateChannelUID)) {
406 updateState(awakeStateChannelUID, OnOffType.from(awakeState));
408 if (!awakeState && !prevDeviceAwake) {
409 // abort refresh channels while device is sleeping, throws many timeouts
410 logger.debug("device {} is sleeping", config.ip);
414 handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_VOLUME_CHANNEL), RefreshType.REFRESH);
415 } catch (AndroidDebugBridgeDeviceReadException e) {
416 logger.warn("Unable to refresh media volume: {}", e.getMessage());
417 } catch (TimeoutException e) {
418 logger.warn("Unable to refresh media volume: Timeout");
421 handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_CONTROL_CHANNEL), RefreshType.REFRESH);
422 } catch (AndroidDebugBridgeDeviceReadException e) {
423 logger.warn("Unable to refresh play status: {}", e.getMessage());
424 } catch (TimeoutException e) {
425 logger.warn("Unable to refresh play status: Timeout");
428 handleCommandInternal(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL), RefreshType.REFRESH);
429 } catch (AndroidDebugBridgeDeviceReadException e) {
430 logger.warn("Unable to refresh current package: {}", e.getMessage());
431 } catch (TimeoutException e) {
432 logger.warn("Unable to refresh current package: Timeout");
435 handleCommandInternal(new ChannelUID(this.thing.getUID(), WAKE_LOCK_CHANNEL), RefreshType.REFRESH);
436 } catch (AndroidDebugBridgeDeviceReadException e) {
437 logger.warn("Unable to refresh wake lock: {}", e.getMessage());
438 } catch (TimeoutException e) {
439 logger.warn("Unable to refresh wake lock: Timeout");
442 handleCommandInternal(new ChannelUID(this.thing.getUID(), SCREEN_STATE_CHANNEL), RefreshType.REFRESH);
443 } catch (AndroidDebugBridgeDeviceReadException e) {
444 logger.warn("Unable to refresh screen state: {}", e.getMessage());
445 } catch (TimeoutException e) {
446 logger.warn("Unable to refresh screen state: Timeout");
450 static class AndroidDebugBridgeMediaStatePackageConfig {
451 public String name = "";
452 public String mode = "";
453 public List<Integer> wakeLockPlayStates = List.of();