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.*;
27 import org.openhab.core.thing.ChannelUID;
28 import org.openhab.core.thing.Thing;
29 import org.openhab.core.thing.ThingStatus;
30 import org.openhab.core.thing.ThingStatusDetail;
31 import org.openhab.core.thing.binding.BaseThingHandler;
32 import org.openhab.core.types.Command;
33 import org.openhab.core.types.RefreshType;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
37 import com.google.gson.Gson;
38 import com.google.gson.JsonSyntaxException;
41 * The {@link AndroidDebugBridgeHandler} is responsible for handling commands, which are
42 * sent to one of the channels.
44 * @author Miguel Álvarez - Initial contribution
47 public class AndroidDebugBridgeHandler extends BaseThingHandler {
49 public static final String KEY_EVENT_PLAY = "126";
50 public static final String KEY_EVENT_PAUSE = "127";
51 public static final String KEY_EVENT_NEXT = "87";
52 public static final String KEY_EVENT_PREVIOUS = "88";
53 public static final String KEY_EVENT_MEDIA_REWIND = "89";
54 public static final String KEY_EVENT_MEDIA_FAST_FORWARD = "90";
55 private static final Gson GSON = new Gson();
56 private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeHandler.class);
57 private final AndroidDebugBridgeDevice adbConnection;
58 private int maxMediaVolume = 0;
59 private AndroidDebugBridgeConfiguration config = new AndroidDebugBridgeConfiguration();
60 private @Nullable ScheduledFuture<?> connectionCheckerSchedule;
61 private AndroidDebugBridgeMediaStatePackageConfig @Nullable [] packageConfigs = null;
63 public AndroidDebugBridgeHandler(Thing thing) {
65 this.adbConnection = new AndroidDebugBridgeDevice(scheduler);
69 public void handleCommand(ChannelUID channelUID, Command command) {
70 var currentConfig = config;
71 if (currentConfig == null) {
75 if (!adbConnection.isConnected()) {
77 adbConnection.connect();
79 handleCommandInternal(channelUID, command);
80 } catch (InterruptedException ignored) {
81 } catch (AndroidDebugBridgeDeviceException | ExecutionException e) {
82 if (!(e.getCause() instanceof InterruptedException)) {
83 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
84 adbConnection.disconnect();
86 } catch (AndroidDebugBridgeDeviceReadException e) {
87 logger.warn("{} - read error: {}", currentConfig.ip, e.getMessage());
88 } catch (TimeoutException e) {
89 logger.warn("{} - timeout error", currentConfig.ip);
93 private void handleCommandInternal(ChannelUID channelUID, Command command)
94 throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
95 TimeoutException, ExecutionException {
96 if (!isLinked(channelUID)) {
99 String channelId = channelUID.getId();
101 case KEY_EVENT_CHANNEL:
102 adbConnection.sendKeyEvent(command.toFullString());
105 adbConnection.sendText(command.toFullString());
107 case MEDIA_VOLUME_CHANNEL:
108 handleMediaVolume(channelUID, command);
110 case MEDIA_CONTROL_CHANNEL:
111 handleMediaControlCommand(channelUID, command);
113 case START_PACKAGE_CHANNEL:
114 adbConnection.startPackage(command.toFullString());
115 updateState(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL),
116 new StringType(command.toFullString()));
118 case STOP_PACKAGE_CHANNEL:
119 adbConnection.stopPackage(command.toFullString());
121 case STOP_CURRENT_PACKAGE_CHANNEL:
122 if (OnOffType.from(command.toFullString()).equals(OnOffType.OFF)) {
123 adbConnection.stopPackage(adbConnection.getCurrentPackage());
126 case CURRENT_PACKAGE_CHANNEL:
127 if (command instanceof RefreshType) {
128 var packageName = adbConnection.getCurrentPackage();
129 updateState(channelUID, new StringType(packageName));
132 case WAKE_LOCK_CHANNEL:
133 if (command instanceof RefreshType) {
134 int lock = adbConnection.getPowerWakeLock();
135 updateState(channelUID, new DecimalType(lock));
138 case SCREEN_STATE_CHANNEL:
139 if (command instanceof RefreshType) {
140 boolean screenState = adbConnection.isScreenOn();
141 updateState(channelUID, OnOffType.from(screenState));
147 private void handleMediaVolume(ChannelUID channelUID, Command command)
148 throws InterruptedException, AndroidDebugBridgeDeviceReadException, AndroidDebugBridgeDeviceException,
149 TimeoutException, ExecutionException {
150 if (command instanceof RefreshType) {
151 var volumeInfo = adbConnection.getMediaVolume();
152 maxMediaVolume = volumeInfo.max;
153 updateState(channelUID, new PercentType((int) Math.round(toPercent(volumeInfo.current, volumeInfo.max))));
155 if (maxMediaVolume == 0) {
156 return; // We can not transform percentage
158 int targetVolume = Integer.parseInt(command.toFullString());
159 adbConnection.setMediaVolume((int) Math.round(fromPercent(targetVolume, maxMediaVolume)));
160 updateState(channelUID, new PercentType(targetVolume));
164 private double toPercent(double value, double maxValue) {
165 return (value / maxValue) * 100;
168 private double fromPercent(double value, double maxValue) {
169 return (value / 100) * maxValue;
172 private void handleMediaControlCommand(ChannelUID channelUID, Command command)
173 throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
174 TimeoutException, ExecutionException {
175 if (command instanceof RefreshType) {
177 String currentPackage = adbConnection.getCurrentPackage();
178 var currentPackageConfig = packageConfigs != null ? Arrays.stream(packageConfigs)
179 .filter(pc -> pc.name.equals(currentPackage)).findFirst().orElse(null) : null;
180 if (currentPackageConfig != null) {
181 logger.debug("media stream config found for {}, mode: {}", currentPackage, currentPackageConfig.mode);
182 switch (currentPackageConfig.mode) {
187 int wakeLockState = adbConnection.getPowerWakeLock();
188 playing = currentPackageConfig.wakeLockPlayStates.contains(wakeLockState);
191 playing = adbConnection.isPlayingMedia(currentPackage);
194 playing = adbConnection.isPlayingAudio();
197 logger.warn("media state config: package {} unsupported mode", currentPackage);
201 logger.debug("media stream config not found for {}", currentPackage);
202 playing = adbConnection.isPlayingMedia(currentPackage);
204 updateState(channelUID, playing ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
205 } else if (command instanceof PlayPauseType) {
206 if (command == PlayPauseType.PLAY) {
207 adbConnection.sendKeyEvent(KEY_EVENT_PLAY);
208 updateState(channelUID, PlayPauseType.PLAY);
209 } else if (command == PlayPauseType.PAUSE) {
210 adbConnection.sendKeyEvent(KEY_EVENT_PAUSE);
211 updateState(channelUID, PlayPauseType.PAUSE);
213 } else if (command instanceof NextPreviousType) {
214 if (command == NextPreviousType.NEXT) {
215 adbConnection.sendKeyEvent(KEY_EVENT_NEXT);
216 } else if (command == NextPreviousType.PREVIOUS) {
217 adbConnection.sendKeyEvent(KEY_EVENT_PREVIOUS);
219 } else if (command instanceof RewindFastforwardType) {
220 if (command == RewindFastforwardType.FASTFORWARD) {
221 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_FAST_FORWARD);
222 } else if (command == RewindFastforwardType.REWIND) {
223 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_REWIND);
226 logger.warn("Unknown media control command: {}", command);
231 public void initialize() {
232 var currentConfig = getConfigAs(AndroidDebugBridgeConfiguration.class);
233 config = currentConfig;
234 var mediaStateJSONConfig = currentConfig.mediaStateJSONConfig;
235 if (mediaStateJSONConfig != null && !mediaStateJSONConfig.isEmpty()) {
236 loadMediaStateConfig(mediaStateJSONConfig);
238 adbConnection.configure(currentConfig.ip, currentConfig.port, currentConfig.timeout);
239 updateStatus(ThingStatus.UNKNOWN);
240 connectionCheckerSchedule = scheduler.scheduleWithFixedDelay(this::checkConnection, 0,
241 currentConfig.refreshTime, TimeUnit.SECONDS);
244 private void loadMediaStateConfig(String mediaStateJSONConfig) {
246 this.packageConfigs = GSON.fromJson(mediaStateJSONConfig,
247 AndroidDebugBridgeMediaStatePackageConfig[].class);
248 } catch (JsonSyntaxException e) {
249 logger.warn("unable to parse media state config: {}", e.getMessage());
254 public void dispose() {
255 var schedule = connectionCheckerSchedule;
256 if (schedule != null) {
257 schedule.cancel(true);
258 connectionCheckerSchedule = null;
260 packageConfigs = null;
261 adbConnection.disconnect();
265 public void checkConnection() {
266 var currentConfig = config;
267 if (currentConfig == null)
270 logger.debug("Refresh device {} status", currentConfig.ip);
271 if (adbConnection.isConnected()) {
272 updateStatus(ThingStatus.ONLINE);
276 adbConnection.connect();
277 } catch (AndroidDebugBridgeDeviceException e) {
278 logger.debug("Error connecting to device; [{}]: {}", e.getClass().getCanonicalName(),
280 updateStatus(ThingStatus.OFFLINE);
283 if (adbConnection.isConnected()) {
284 updateStatus(ThingStatus.ONLINE);
288 } catch (InterruptedException ignored) {
289 } catch (AndroidDebugBridgeDeviceException | ExecutionException e) {
290 logger.debug("Connection checker error: {}", e.getMessage());
291 adbConnection.disconnect();
292 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
296 private void refreshStatus() throws InterruptedException, AndroidDebugBridgeDeviceException, ExecutionException {
298 handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_VOLUME_CHANNEL), RefreshType.REFRESH);
299 } catch (AndroidDebugBridgeDeviceReadException e) {
300 logger.warn("Unable to refresh media volume: {}", e.getMessage());
301 } catch (TimeoutException e) {
302 logger.warn("Unable to refresh media volume: Timeout");
305 handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_CONTROL_CHANNEL), RefreshType.REFRESH);
306 } catch (AndroidDebugBridgeDeviceReadException e) {
307 logger.warn("Unable to refresh play status: {}", e.getMessage());
308 } catch (TimeoutException e) {
309 logger.warn("Unable to refresh play status: Timeout");
312 handleCommandInternal(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL), RefreshType.REFRESH);
313 } catch (AndroidDebugBridgeDeviceReadException e) {
314 logger.warn("Unable to refresh current package: {}", e.getMessage());
315 } catch (TimeoutException e) {
316 logger.warn("Unable to refresh current package: Timeout");
319 handleCommandInternal(new ChannelUID(this.thing.getUID(), WAKE_LOCK_CHANNEL), RefreshType.REFRESH);
320 } catch (AndroidDebugBridgeDeviceReadException e) {
321 logger.warn("Unable to refresh wake lock: {}", e.getMessage());
322 } catch (TimeoutException e) {
323 logger.warn("Unable to refresh wake lock: Timeout");
326 handleCommandInternal(new ChannelUID(this.thing.getUID(), SCREEN_STATE_CHANNEL), RefreshType.REFRESH);
327 } catch (AndroidDebugBridgeDeviceReadException e) {
328 logger.warn("Unable to refresh screen state: {}", e.getMessage());
329 } catch (TimeoutException e) {
330 logger.warn("Unable to refresh screen state: Timeout");
334 static class AndroidDebugBridgeMediaStatePackageConfig {
335 public String name = "";
336 public String mode = "";
337 public List<Integer> wakeLockPlayStates = List.of();