2 * Copyright (c) 2010-2021 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;
19 import java.util.concurrent.ExecutionException;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.TimeoutException;
23 import java.util.regex.Pattern;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.core.library.types.DecimalType;
28 import org.openhab.core.library.types.NextPreviousType;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.library.types.PercentType;
31 import org.openhab.core.library.types.PlayPauseType;
32 import org.openhab.core.library.types.RewindFastforwardType;
33 import org.openhab.core.library.types.StringType;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.types.Command;
40 import org.openhab.core.types.RefreshType;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
44 import com.google.gson.Gson;
45 import com.google.gson.JsonSyntaxException;
48 * The {@link AndroidDebugBridgeHandler} is responsible for handling commands, which are
49 * sent to one of the channels.
51 * @author Miguel Álvarez - Initial contribution
54 public class AndroidDebugBridgeHandler extends BaseThingHandler {
56 public static final String KEY_EVENT_PLAY = "126";
57 public static final String KEY_EVENT_PAUSE = "127";
58 public static final String KEY_EVENT_NEXT = "87";
59 public static final String KEY_EVENT_PREVIOUS = "88";
60 public static final String KEY_EVENT_MEDIA_REWIND = "89";
61 public static final String KEY_EVENT_MEDIA_FAST_FORWARD = "90";
62 private static final String SHUTDOWN_POWER_OFF = "POWER_OFF";
63 private static final String SHUTDOWN_REBOOT = "REBOOT";
64 private static final Gson GSON = new Gson();
65 private static final Pattern RECORD_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]*$");
66 private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeHandler.class);
67 private final AndroidDebugBridgeDevice adbConnection;
68 private int maxMediaVolume = 0;
69 private AndroidDebugBridgeConfiguration config = new AndroidDebugBridgeConfiguration();
70 private @Nullable ScheduledFuture<?> connectionCheckerSchedule;
71 private AndroidDebugBridgeMediaStatePackageConfig @Nullable [] packageConfigs = null;
72 private boolean deviceAwake = false;
74 public AndroidDebugBridgeHandler(Thing thing) {
76 this.adbConnection = new AndroidDebugBridgeDevice(scheduler);
80 public void handleCommand(ChannelUID channelUID, Command command) {
81 var currentConfig = config;
82 if (currentConfig == null) {
86 if (!adbConnection.isConnected()) {
88 adbConnection.connect();
90 handleCommandInternal(channelUID, command);
91 } catch (InterruptedException ignored) {
92 } catch (AndroidDebugBridgeDeviceException | ExecutionException e) {
93 if (!(e.getCause() instanceof InterruptedException)) {
94 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
95 adbConnection.disconnect();
97 } catch (AndroidDebugBridgeDeviceReadException e) {
98 logger.warn("{} - read error: {}", currentConfig.ip, e.getMessage());
99 } catch (TimeoutException e) {
100 logger.warn("{} - timeout error", currentConfig.ip);
104 private void handleCommandInternal(ChannelUID channelUID, Command command)
105 throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
106 TimeoutException, ExecutionException {
107 if (!isLinked(channelUID)) {
110 String channelId = channelUID.getId();
112 case KEY_EVENT_CHANNEL:
113 adbConnection.sendKeyEvent(command.toFullString());
116 adbConnection.sendText(command.toFullString());
119 adbConnection.sendTap(command.toFullString());
122 adbConnection.openUrl(command.toFullString());
124 case MEDIA_VOLUME_CHANNEL:
125 handleMediaVolume(channelUID, command);
127 case MEDIA_CONTROL_CHANNEL:
128 handleMediaControlCommand(channelUID, command);
130 case START_PACKAGE_CHANNEL:
131 adbConnection.startPackage(command.toFullString());
132 updateState(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL),
133 new StringType(command.toFullString()));
135 case STOP_PACKAGE_CHANNEL:
136 adbConnection.stopPackage(command.toFullString());
138 case STOP_CURRENT_PACKAGE_CHANNEL:
139 if (OnOffType.from(command.toFullString()).equals(OnOffType.OFF)) {
140 adbConnection.stopPackage(adbConnection.getCurrentPackage());
143 case CURRENT_PACKAGE_CHANNEL:
144 if (command instanceof RefreshType) {
145 var packageName = adbConnection.getCurrentPackage();
146 updateState(channelUID, new StringType(packageName));
149 case WAKE_LOCK_CHANNEL:
150 if (command instanceof RefreshType) {
151 int lock = adbConnection.getPowerWakeLock();
152 updateState(channelUID, new DecimalType(lock));
155 case AWAKE_STATE_CHANNEL:
156 if (command instanceof RefreshType) {
157 boolean awakeState = adbConnection.isAwake();
158 updateState(channelUID, OnOffType.from(awakeState));
161 case SCREEN_STATE_CHANNEL:
162 if (command instanceof RefreshType) {
163 boolean screenState = adbConnection.isScreenOn();
164 updateState(channelUID, OnOffType.from(screenState));
167 case SHUTDOWN_CHANNEL:
168 switch (command.toFullString()) {
169 case SHUTDOWN_POWER_OFF:
170 adbConnection.powerOffDevice();
171 updateStatus(ThingStatus.OFFLINE);
173 case SHUTDOWN_REBOOT:
174 adbConnection.rebootDevice();
175 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Rebooting");
179 case RECORD_INPUT_CHANNEL:
180 recordDeviceInput(command);
182 case RECORDED_INPUT_CHANNEL:
183 String recordName = getRecordPropertyName(command);
184 var inputCommand = this.getThing().getProperties().get(recordName);
185 if (inputCommand != null) {
186 adbConnection.sendInputEvents(inputCommand);
192 private void recordDeviceInput(Command recordNameCommand)
193 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
194 var recordName = recordNameCommand.toFullString();
195 if (!RECORD_NAME_PATTERN.matcher(recordName).matches()) {
196 logger.warn("Invalid record name, accepts alphanumeric values with '_'.");
199 String recordPropertyName = getRecordPropertyName(recordName);
200 logger.debug("RECORD: {}", recordPropertyName);
201 var eventCommand = adbConnection.recordInputEvents();
202 if (eventCommand.isEmpty()) {
203 logger.debug("No events recorded");
204 if (this.getThing().getProperties().containsKey(recordPropertyName)) {
205 this.getThing().setProperty(recordPropertyName, null);
206 updateProperties(editProperties());
207 logger.debug("Record {} deleted", recordName);
210 updateProperty(recordPropertyName, eventCommand);
211 logger.debug("New record {}: {}", recordName, eventCommand);
215 private String getRecordPropertyName(String recordName) {
216 return String.format("input-record:%s", recordName);
219 private String getRecordPropertyName(Command recordNameCommand) {
220 return getRecordPropertyName(recordNameCommand.toFullString());
223 private void handleMediaVolume(ChannelUID channelUID, Command command)
224 throws InterruptedException, AndroidDebugBridgeDeviceReadException, AndroidDebugBridgeDeviceException,
225 TimeoutException, ExecutionException {
226 if (command instanceof RefreshType) {
227 var volumeInfo = adbConnection.getMediaVolume();
228 maxMediaVolume = volumeInfo.max;
229 updateState(channelUID, new PercentType((int) Math.round(toPercent(volumeInfo.current, volumeInfo.max))));
231 if (maxMediaVolume == 0) {
232 return; // We can not transform percentage
234 int targetVolume = Integer.parseInt(command.toFullString());
235 adbConnection.setMediaVolume((int) Math.round(fromPercent(targetVolume, maxMediaVolume)));
236 updateState(channelUID, new PercentType(targetVolume));
240 private double toPercent(double value, double maxValue) {
241 return (value / maxValue) * 100;
244 private double fromPercent(double value, double maxValue) {
245 return (value / 100) * maxValue;
248 private void handleMediaControlCommand(ChannelUID channelUID, Command command)
249 throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
250 TimeoutException, ExecutionException {
251 if (command instanceof RefreshType) {
253 String currentPackage = adbConnection.getCurrentPackage();
254 var currentPackageConfig = packageConfigs != null ? Arrays.stream(packageConfigs)
255 .filter(pc -> pc.name.equals(currentPackage)).findFirst().orElse(null) : null;
256 if (currentPackageConfig != null) {
257 logger.debug("media stream config found for {}, mode: {}", currentPackage, currentPackageConfig.mode);
258 switch (currentPackageConfig.mode) {
263 int wakeLockState = adbConnection.getPowerWakeLock();
264 playing = currentPackageConfig.wakeLockPlayStates.contains(wakeLockState);
267 playing = adbConnection.isPlayingMedia(currentPackage);
270 playing = adbConnection.isPlayingAudio();
273 logger.warn("media state config: package {} unsupported mode", currentPackage);
277 logger.debug("media stream config not found for {}", currentPackage);
278 playing = adbConnection.isPlayingMedia(currentPackage);
280 updateState(channelUID, playing ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
281 } else if (command instanceof PlayPauseType) {
282 if (command == PlayPauseType.PLAY) {
283 adbConnection.sendKeyEvent(KEY_EVENT_PLAY);
284 updateState(channelUID, PlayPauseType.PLAY);
285 } else if (command == PlayPauseType.PAUSE) {
286 adbConnection.sendKeyEvent(KEY_EVENT_PAUSE);
287 updateState(channelUID, PlayPauseType.PAUSE);
289 } else if (command instanceof NextPreviousType) {
290 if (command == NextPreviousType.NEXT) {
291 adbConnection.sendKeyEvent(KEY_EVENT_NEXT);
292 } else if (command == NextPreviousType.PREVIOUS) {
293 adbConnection.sendKeyEvent(KEY_EVENT_PREVIOUS);
295 } else if (command instanceof RewindFastforwardType) {
296 if (command == RewindFastforwardType.FASTFORWARD) {
297 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_FAST_FORWARD);
298 } else if (command == RewindFastforwardType.REWIND) {
299 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_REWIND);
302 logger.warn("Unknown media control command: {}", command);
307 public void initialize() {
308 var currentConfig = getConfigAs(AndroidDebugBridgeConfiguration.class);
309 config = currentConfig;
310 var mediaStateJSONConfig = currentConfig.mediaStateJSONConfig;
311 if (mediaStateJSONConfig != null && !mediaStateJSONConfig.isEmpty()) {
312 loadMediaStateConfig(mediaStateJSONConfig);
314 adbConnection.configure(currentConfig.ip, currentConfig.port, currentConfig.timeout,
315 currentConfig.recordDuration);
316 updateStatus(ThingStatus.UNKNOWN);
317 connectionCheckerSchedule = scheduler.scheduleWithFixedDelay(this::checkConnection, 0,
318 currentConfig.refreshTime, TimeUnit.SECONDS);
321 private void loadMediaStateConfig(String mediaStateJSONConfig) {
323 this.packageConfigs = GSON.fromJson(mediaStateJSONConfig,
324 AndroidDebugBridgeMediaStatePackageConfig[].class);
325 } catch (JsonSyntaxException e) {
326 logger.warn("unable to parse media state config: {}", e.getMessage());
331 public void dispose() {
332 var schedule = connectionCheckerSchedule;
333 if (schedule != null) {
334 schedule.cancel(true);
335 connectionCheckerSchedule = null;
337 packageConfigs = null;
338 adbConnection.disconnect();
342 public void checkConnection() {
343 var currentConfig = config;
344 if (currentConfig == null) {
348 logger.debug("Refresh device {} status", currentConfig.ip);
349 if (adbConnection.isConnected()) {
350 updateStatus(ThingStatus.ONLINE);
354 adbConnection.connect();
355 } catch (AndroidDebugBridgeDeviceException e) {
356 logger.debug("Error connecting to device; [{}]: {}", e.getClass().getCanonicalName(),
358 adbConnection.disconnect();
359 updateStatus(ThingStatus.OFFLINE);
362 if (adbConnection.isConnected()) {
363 updateStatus(ThingStatus.ONLINE);
367 } catch (InterruptedException ignored) {
368 } catch (AndroidDebugBridgeDeviceException | ExecutionException e) {
369 logger.debug("Connection checker error: {}", e.getMessage());
370 adbConnection.disconnect();
371 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
375 private void refreshStatus() throws InterruptedException, AndroidDebugBridgeDeviceException, ExecutionException {
377 boolean prevDeviceAwake = deviceAwake;
379 awakeState = adbConnection.isAwake();
380 deviceAwake = awakeState;
381 } catch (TimeoutException e) {
382 // happen a lot when device is sleeping; abort refresh other channels
383 logger.debug("Unable to refresh awake state: Timeout; aborting channels refresh");
386 var awakeStateChannelUID = new ChannelUID(this.thing.getUID(), AWAKE_STATE_CHANNEL);
387 if (isLinked(awakeStateChannelUID)) {
388 updateState(awakeStateChannelUID, OnOffType.from(awakeState));
390 if (!awakeState && !prevDeviceAwake) {
391 // abort refresh channels while device is sleeping, throws many timeouts
392 logger.debug("device {} is sleeping", config.ip);
396 handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_VOLUME_CHANNEL), RefreshType.REFRESH);
397 } catch (AndroidDebugBridgeDeviceReadException e) {
398 logger.warn("Unable to refresh media volume: {}", e.getMessage());
399 } catch (TimeoutException e) {
400 logger.warn("Unable to refresh media volume: Timeout");
403 handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_CONTROL_CHANNEL), RefreshType.REFRESH);
404 } catch (AndroidDebugBridgeDeviceReadException e) {
405 logger.warn("Unable to refresh play status: {}", e.getMessage());
406 } catch (TimeoutException e) {
407 logger.warn("Unable to refresh play status: Timeout");
410 handleCommandInternal(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL), RefreshType.REFRESH);
411 } catch (AndroidDebugBridgeDeviceReadException e) {
412 logger.warn("Unable to refresh current package: {}", e.getMessage());
413 } catch (TimeoutException e) {
414 logger.warn("Unable to refresh current package: Timeout");
417 handleCommandInternal(new ChannelUID(this.thing.getUID(), WAKE_LOCK_CHANNEL), RefreshType.REFRESH);
418 } catch (AndroidDebugBridgeDeviceReadException e) {
419 logger.warn("Unable to refresh wake lock: {}", e.getMessage());
420 } catch (TimeoutException e) {
421 logger.warn("Unable to refresh wake lock: Timeout");
424 handleCommandInternal(new ChannelUID(this.thing.getUID(), SCREEN_STATE_CHANNEL), RefreshType.REFRESH);
425 } catch (AndroidDebugBridgeDeviceReadException e) {
426 logger.warn("Unable to refresh screen state: {}", e.getMessage());
427 } catch (TimeoutException e) {
428 logger.warn("Unable to refresh screen state: Timeout");
432 static class AndroidDebugBridgeMediaStatePackageConfig {
433 public String name = "";
434 public String mode = "";
435 public List<Integer> wakeLockPlayStates = List.of();