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;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.core.library.types.DecimalType;
27 import org.openhab.core.library.types.NextPreviousType;
28 import org.openhab.core.library.types.OnOffType;
29 import org.openhab.core.library.types.PercentType;
30 import org.openhab.core.library.types.PlayPauseType;
31 import org.openhab.core.library.types.RewindFastforwardType;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.binding.BaseThingHandler;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import com.google.gson.Gson;
44 import com.google.gson.JsonSyntaxException;
47 * The {@link AndroidDebugBridgeHandler} is responsible for handling commands, which are
48 * sent to one of the channels.
50 * @author Miguel Álvarez - Initial contribution
53 public class AndroidDebugBridgeHandler extends BaseThingHandler {
55 public static final String KEY_EVENT_PLAY = "126";
56 public static final String KEY_EVENT_PAUSE = "127";
57 public static final String KEY_EVENT_NEXT = "87";
58 public static final String KEY_EVENT_PREVIOUS = "88";
59 public static final String KEY_EVENT_MEDIA_REWIND = "89";
60 public static final String KEY_EVENT_MEDIA_FAST_FORWARD = "90";
61 private static final String SHUTDOWN_POWER_OFF = "POWER_OFF";
62 private static final String SHUTDOWN_REBOOT = "REBOOT";
63 private static final Gson GSON = new Gson();
64 private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeHandler.class);
65 private final AndroidDebugBridgeDevice adbConnection;
66 private int maxMediaVolume = 0;
67 private AndroidDebugBridgeConfiguration config = new AndroidDebugBridgeConfiguration();
68 private @Nullable ScheduledFuture<?> connectionCheckerSchedule;
69 private AndroidDebugBridgeMediaStatePackageConfig @Nullable [] packageConfigs = null;
70 private boolean deviceAwake = false;
72 public AndroidDebugBridgeHandler(Thing thing) {
74 this.adbConnection = new AndroidDebugBridgeDevice(scheduler);
78 public void handleCommand(ChannelUID channelUID, Command command) {
79 var currentConfig = config;
80 if (currentConfig == null) {
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());
119 case MEDIA_VOLUME_CHANNEL:
120 handleMediaVolume(channelUID, command);
122 case MEDIA_CONTROL_CHANNEL:
123 handleMediaControlCommand(channelUID, command);
125 case START_PACKAGE_CHANNEL:
126 adbConnection.startPackage(command.toFullString());
127 updateState(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL),
128 new StringType(command.toFullString()));
130 case STOP_PACKAGE_CHANNEL:
131 adbConnection.stopPackage(command.toFullString());
133 case STOP_CURRENT_PACKAGE_CHANNEL:
134 if (OnOffType.from(command.toFullString()).equals(OnOffType.OFF)) {
135 adbConnection.stopPackage(adbConnection.getCurrentPackage());
138 case CURRENT_PACKAGE_CHANNEL:
139 if (command instanceof RefreshType) {
140 var packageName = adbConnection.getCurrentPackage();
141 updateState(channelUID, new StringType(packageName));
144 case WAKE_LOCK_CHANNEL:
145 if (command instanceof RefreshType) {
146 int lock = adbConnection.getPowerWakeLock();
147 updateState(channelUID, new DecimalType(lock));
150 case AWAKE_STATE_CHANNEL:
151 if (command instanceof RefreshType) {
152 boolean awakeState = adbConnection.isAwake();
153 updateState(channelUID, OnOffType.from(awakeState));
156 case SCREEN_STATE_CHANNEL:
157 if (command instanceof RefreshType) {
158 boolean screenState = adbConnection.isScreenOn();
159 updateState(channelUID, OnOffType.from(screenState));
162 case SHUTDOWN_CHANNEL:
163 switch (command.toFullString()) {
164 case SHUTDOWN_POWER_OFF:
165 adbConnection.powerOffDevice();
166 updateStatus(ThingStatus.OFFLINE);
168 case SHUTDOWN_REBOOT:
169 adbConnection.rebootDevice();
170 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Rebooting");
176 private void handleMediaVolume(ChannelUID channelUID, Command command)
177 throws InterruptedException, AndroidDebugBridgeDeviceReadException, AndroidDebugBridgeDeviceException,
178 TimeoutException, ExecutionException {
179 if (command instanceof RefreshType) {
180 var volumeInfo = adbConnection.getMediaVolume();
181 maxMediaVolume = volumeInfo.max;
182 updateState(channelUID, new PercentType((int) Math.round(toPercent(volumeInfo.current, volumeInfo.max))));
184 if (maxMediaVolume == 0) {
185 return; // We can not transform percentage
187 int targetVolume = Integer.parseInt(command.toFullString());
188 adbConnection.setMediaVolume((int) Math.round(fromPercent(targetVolume, maxMediaVolume)));
189 updateState(channelUID, new PercentType(targetVolume));
193 private double toPercent(double value, double maxValue) {
194 return (value / maxValue) * 100;
197 private double fromPercent(double value, double maxValue) {
198 return (value / 100) * maxValue;
201 private void handleMediaControlCommand(ChannelUID channelUID, Command command)
202 throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
203 TimeoutException, ExecutionException {
204 if (command instanceof RefreshType) {
206 String currentPackage = adbConnection.getCurrentPackage();
207 var currentPackageConfig = packageConfigs != null ? Arrays.stream(packageConfigs)
208 .filter(pc -> pc.name.equals(currentPackage)).findFirst().orElse(null) : null;
209 if (currentPackageConfig != null) {
210 logger.debug("media stream config found for {}, mode: {}", currentPackage, currentPackageConfig.mode);
211 switch (currentPackageConfig.mode) {
216 int wakeLockState = adbConnection.getPowerWakeLock();
217 playing = currentPackageConfig.wakeLockPlayStates.contains(wakeLockState);
220 playing = adbConnection.isPlayingMedia(currentPackage);
223 playing = adbConnection.isPlayingAudio();
226 logger.warn("media state config: package {} unsupported mode", currentPackage);
230 logger.debug("media stream config not found for {}", currentPackage);
231 playing = adbConnection.isPlayingMedia(currentPackage);
233 updateState(channelUID, playing ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
234 } else if (command instanceof PlayPauseType) {
235 if (command == PlayPauseType.PLAY) {
236 adbConnection.sendKeyEvent(KEY_EVENT_PLAY);
237 updateState(channelUID, PlayPauseType.PLAY);
238 } else if (command == PlayPauseType.PAUSE) {
239 adbConnection.sendKeyEvent(KEY_EVENT_PAUSE);
240 updateState(channelUID, PlayPauseType.PAUSE);
242 } else if (command instanceof NextPreviousType) {
243 if (command == NextPreviousType.NEXT) {
244 adbConnection.sendKeyEvent(KEY_EVENT_NEXT);
245 } else if (command == NextPreviousType.PREVIOUS) {
246 adbConnection.sendKeyEvent(KEY_EVENT_PREVIOUS);
248 } else if (command instanceof RewindFastforwardType) {
249 if (command == RewindFastforwardType.FASTFORWARD) {
250 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_FAST_FORWARD);
251 } else if (command == RewindFastforwardType.REWIND) {
252 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_REWIND);
255 logger.warn("Unknown media control command: {}", command);
260 public void initialize() {
261 var currentConfig = getConfigAs(AndroidDebugBridgeConfiguration.class);
262 config = currentConfig;
263 var mediaStateJSONConfig = currentConfig.mediaStateJSONConfig;
264 if (mediaStateJSONConfig != null && !mediaStateJSONConfig.isEmpty()) {
265 loadMediaStateConfig(mediaStateJSONConfig);
267 adbConnection.configure(currentConfig.ip, currentConfig.port, currentConfig.timeout);
268 updateStatus(ThingStatus.UNKNOWN);
269 connectionCheckerSchedule = scheduler.scheduleWithFixedDelay(this::checkConnection, 0,
270 currentConfig.refreshTime, TimeUnit.SECONDS);
273 private void loadMediaStateConfig(String mediaStateJSONConfig) {
275 this.packageConfigs = GSON.fromJson(mediaStateJSONConfig,
276 AndroidDebugBridgeMediaStatePackageConfig[].class);
277 } catch (JsonSyntaxException e) {
278 logger.warn("unable to parse media state config: {}", e.getMessage());
283 public void dispose() {
284 var schedule = connectionCheckerSchedule;
285 if (schedule != null) {
286 schedule.cancel(true);
287 connectionCheckerSchedule = null;
289 packageConfigs = null;
290 adbConnection.disconnect();
294 public void checkConnection() {
295 var currentConfig = config;
296 if (currentConfig == null) {
300 logger.debug("Refresh device {} status", currentConfig.ip);
301 if (adbConnection.isConnected()) {
302 updateStatus(ThingStatus.ONLINE);
306 adbConnection.connect();
307 } catch (AndroidDebugBridgeDeviceException e) {
308 logger.debug("Error connecting to device; [{}]: {}", e.getClass().getCanonicalName(),
310 adbConnection.disconnect();
311 updateStatus(ThingStatus.OFFLINE);
314 if (adbConnection.isConnected()) {
315 updateStatus(ThingStatus.ONLINE);
319 } catch (InterruptedException ignored) {
320 } catch (AndroidDebugBridgeDeviceException | ExecutionException e) {
321 logger.debug("Connection checker error: {}", e.getMessage());
322 adbConnection.disconnect();
323 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
327 private void refreshStatus() throws InterruptedException, AndroidDebugBridgeDeviceException, ExecutionException {
329 boolean prevDeviceAwake = deviceAwake;
331 awakeState = adbConnection.isAwake();
332 deviceAwake = awakeState;
333 } catch (TimeoutException e) {
334 logger.warn("Unable to refresh awake state: Timeout");
337 var awakeStateChannelUID = new ChannelUID(this.thing.getUID(), AWAKE_STATE_CHANNEL);
338 if (isLinked(awakeStateChannelUID)) {
339 updateState(awakeStateChannelUID, OnOffType.from(awakeState));
341 if (!awakeState && !prevDeviceAwake) {
342 logger.debug("device {} is sleeping", config.ip);
346 handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_VOLUME_CHANNEL), RefreshType.REFRESH);
347 } catch (AndroidDebugBridgeDeviceReadException e) {
348 logger.warn("Unable to refresh media volume: {}", e.getMessage());
349 } catch (TimeoutException e) {
350 logger.warn("Unable to refresh media volume: Timeout");
353 handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_CONTROL_CHANNEL), RefreshType.REFRESH);
354 } catch (AndroidDebugBridgeDeviceReadException e) {
355 logger.warn("Unable to refresh play status: {}", e.getMessage());
356 } catch (TimeoutException e) {
357 logger.warn("Unable to refresh play status: Timeout");
360 handleCommandInternal(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL), RefreshType.REFRESH);
361 } catch (AndroidDebugBridgeDeviceReadException e) {
362 logger.warn("Unable to refresh current package: {}", e.getMessage());
363 } catch (TimeoutException e) {
364 logger.warn("Unable to refresh current package: Timeout");
367 handleCommandInternal(new ChannelUID(this.thing.getUID(), WAKE_LOCK_CHANNEL), RefreshType.REFRESH);
368 } catch (AndroidDebugBridgeDeviceReadException e) {
369 logger.warn("Unable to refresh wake lock: {}", e.getMessage());
370 } catch (TimeoutException e) {
371 logger.warn("Unable to refresh wake lock: Timeout");
374 handleCommandInternal(new ChannelUID(this.thing.getUID(), SCREEN_STATE_CHANNEL), RefreshType.REFRESH);
375 } catch (AndroidDebugBridgeDeviceReadException e) {
376 logger.warn("Unable to refresh screen state: {}", e.getMessage());
377 } catch (TimeoutException e) {
378 logger.warn("Unable to refresh screen state: Timeout");
382 static class AndroidDebugBridgeMediaStatePackageConfig {
383 public String name = "";
384 public String mode = "";
385 public List<Integer> wakeLockPlayStates = List.of();