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;
16 import java.net.InetSocketAddress;
17 import java.net.Socket;
18 import java.net.URLEncoder;
19 import java.nio.charset.Charset;
20 import java.nio.charset.StandardCharsets;
21 import java.security.NoSuchAlgorithmException;
22 import java.security.spec.InvalidKeySpecException;
23 import java.util.Arrays;
24 import java.util.Base64;
25 import java.util.concurrent.*;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.core.OpenHAB;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
35 import com.tananaev.adblib.AdbBase64;
36 import com.tananaev.adblib.AdbConnection;
37 import com.tananaev.adblib.AdbCrypto;
38 import com.tananaev.adblib.AdbStream;
41 * The {@link AndroidDebugBridgeConfiguration} class encapsulates adb device connection logic.
43 * @author Miguel Álvarez - Initial contribution
46 public class AndroidDebugBridgeDevice {
47 public static final int ANDROID_MEDIA_STREAM = 3;
48 private static final String ADB_FOLDER = OpenHAB.getUserDataFolder() + File.separator + ".adb";
49 private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class);
50 private static final Pattern VOLUME_PATTERN = Pattern
51 .compile("volume is (?<current>\\d.*) in range \\[(?<min>\\d.*)\\.\\.(?<max>\\d.*)]");
53 private static @Nullable AdbCrypto adbCrypto;
56 var logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class);
58 File directory = new File(ADB_FOLDER);
59 if (!directory.exists()) {
62 adbCrypto = loadKeyPair(ADB_FOLDER + File.separator + "adb_pub.key",
63 ADB_FOLDER + File.separator + "adb.key");
64 } catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) {
65 logger.warn("Unable to setup adb keys: {}", e.getMessage());
69 private final ScheduledExecutorService scheduler;
71 private String ip = "127.0.0.1";
72 private int port = 5555;
73 private int timeoutSec = 5;
74 private @Nullable Socket socket;
75 private @Nullable AdbConnection connection;
76 private @Nullable Future<String> commandFuture;
78 AndroidDebugBridgeDevice(ScheduledExecutorService scheduler) {
79 this.scheduler = scheduler;
82 public void configure(String ip, int port, int timeout) {
85 this.timeoutSec = timeout;
88 public void sendKeyEvent(String eventCode)
89 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
90 runAdbShell("input", "keyevent", eventCode);
93 public void sendText(String text)
94 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
95 runAdbShell("input", "text", URLEncoder.encode(text, StandardCharsets.UTF_8));
98 public void startPackage(String packageName)
99 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
100 var out = runAdbShell("monkey", "--pct-syskeys", "0", "-p", packageName, "-v", "1");
101 if (out.contains("monkey aborted"))
102 throw new AndroidDebugBridgeDeviceException("Unable to open package");
105 public void stopPackage(String packageName)
106 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
107 runAdbShell("am", "force-stop", packageName);
110 public String getCurrentPackage() throws AndroidDebugBridgeDeviceException, InterruptedException,
111 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
112 var out = runAdbShell("dumpsys", "window", "windows", "|", "grep", "mFocusedApp");
113 var targetLine = Arrays.stream(out.split("\n")).findFirst().orElse("");
114 var lineParts = targetLine.split(" ");
115 if (lineParts.length >= 2) {
116 var packageActivityName = lineParts[lineParts.length - 2];
117 if (packageActivityName.contains("/"))
118 return packageActivityName.split("/")[0];
120 throw new AndroidDebugBridgeDeviceReadException("Unable to read package name");
123 public boolean isAwake()
124 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
125 String devicesResp = runAdbShell("dumpsys", "activity", "|", "grep", "mWakefulness");
126 return devicesResp.contains("mWakefulness=Awake");
129 public boolean isScreenOn() throws InterruptedException, AndroidDebugBridgeDeviceException,
130 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
131 String devicesResp = runAdbShell("dumpsys", "power", "|", "grep", "'Display Power'");
132 if (devicesResp.contains("=")) {
134 var state = devicesResp.split("=")[1].trim();
135 return state.equals("ON");
136 } catch (NumberFormatException e) {
137 logger.debug("Unable to parse device wake lock: {}", e.getMessage());
140 throw new AndroidDebugBridgeDeviceReadException("Unable to read screen state");
143 public boolean isPlayingMedia(String currentApp)
144 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
145 String devicesResp = runAdbShell("dumpsys", "media_session", "|", "grep", "-A", "100", "'Sessions Stack'", "|",
146 "grep", "-A", "50", currentApp);
147 String[] mediaSessions = devicesResp.split("\n\n");
148 if (mediaSessions.length == 0) {
149 // no media session found for current app
152 boolean isPlaying = mediaSessions[0].contains("PlaybackState {state=3");
153 logger.debug("device media state playing {}", isPlaying);
157 public boolean isPlayingAudio()
158 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
159 String audioDump = runAdbShell("dumpsys", "audio", "|", "grep", "ID:");
160 return audioDump.contains("state:started");
163 public VolumeInfo getMediaVolume() throws AndroidDebugBridgeDeviceException, InterruptedException,
164 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
165 return getVolume(ANDROID_MEDIA_STREAM);
168 public void setMediaVolume(int volume)
169 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
170 setVolume(ANDROID_MEDIA_STREAM, volume);
173 public int getPowerWakeLock() throws InterruptedException, AndroidDebugBridgeDeviceException,
174 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
175 String lockResp = runAdbShell("dumpsys", "power", "|", "grep", "Locks", "|", "grep", "'size='");
176 if (lockResp.contains("=")) {
178 return Integer.parseInt(lockResp.replace("\n", "").split("=")[1].trim());
179 } catch (NumberFormatException e) {
180 logger.debug("Unable to parse device wake lock: {}", e.getMessage());
183 throw new AndroidDebugBridgeDeviceReadException("Unable to read wake lock");
186 private void setVolume(int stream, int volume)
187 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
188 runAdbShell("media", "volume", "--show", "--stream", String.valueOf(stream), "--set", String.valueOf(volume));
191 public String getModel() throws AndroidDebugBridgeDeviceException, InterruptedException,
192 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
193 return getDeviceProp("ro.product.model");
196 public String getAndroidVersion() throws AndroidDebugBridgeDeviceException, InterruptedException,
197 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
198 return getDeviceProp("ro.build.version.release");
201 public String getBrand() throws AndroidDebugBridgeDeviceException, InterruptedException,
202 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
203 return getDeviceProp("ro.product.brand");
206 public String getSerialNo() throws AndroidDebugBridgeDeviceException, InterruptedException,
207 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
208 return getDeviceProp("ro.serialno");
211 private String getDeviceProp(String name) throws AndroidDebugBridgeDeviceException, InterruptedException,
212 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
213 var propValue = runAdbShell("getprop", name, "&&", "sleep", "0.3").replace("\n", "").replace("\r", "");
214 if (propValue.length() == 0) {
215 throw new AndroidDebugBridgeDeviceReadException("Unable to get device property");
220 private VolumeInfo getVolume(int stream) throws AndroidDebugBridgeDeviceException, InterruptedException,
221 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
222 String volumeResp = runAdbShell("media", "volume", "--show", "--stream", String.valueOf(stream), "--get", "|",
224 Matcher matcher = VOLUME_PATTERN.matcher(volumeResp);
226 throw new AndroidDebugBridgeDeviceReadException("Unable to get volume info");
227 var volumeInfo = new VolumeInfo(Integer.parseInt(matcher.group("current")),
228 Integer.parseInt(matcher.group("min")), Integer.parseInt(matcher.group("max")));
229 logger.debug("Device {}:{} VolumeInfo: current {}, min {}, max {}", this.ip, this.port, volumeInfo.current,
230 volumeInfo.min, volumeInfo.max);
234 public boolean isConnected() {
235 var currentSocket = socket;
236 return currentSocket != null && currentSocket.isConnected();
239 public void connect() throws AndroidDebugBridgeDeviceException, InterruptedException {
241 AdbConnection adbConnection;
243 AdbCrypto crypto = adbCrypto;
244 if (crypto == null) {
245 throw new AndroidDebugBridgeDeviceException("Device not connected");
250 sock.connect(new InetSocketAddress(ip, port), (int) TimeUnit.SECONDS.toMillis(15));
251 } catch (IOException e) {
252 logger.debug("Error connecting to {}: [{}] {}", ip, e.getClass().getName(), e.getMessage());
253 if (e.getMessage().equals("Socket closed")) {
254 // Connection aborted by us
255 throw new InterruptedException();
257 throw new AndroidDebugBridgeDeviceException("Can not open socket " + ip + ":" + port);
260 adbConnection = AdbConnection.create(sock, crypto);
261 connection = adbConnection;
262 adbConnection.connect(15, TimeUnit.SECONDS, false);
263 } catch (IOException e) {
264 logger.debug("Error connecting to {}: {}", ip, e.getMessage());
265 throw new AndroidDebugBridgeDeviceException("Can not open adb connection " + ip + ":" + port);
269 private String runAdbShell(String... args)
270 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
271 var adb = connection;
273 throw new AndroidDebugBridgeDeviceException("Device not connected");
275 var commandFuture = scheduler.submit(() -> {
276 var byteArrayOutputStream = new ByteArrayOutputStream();
277 String cmd = String.join(" ", args);
278 logger.debug("{} - shell:{}", ip, cmd);
280 AdbStream stream = adb.open("shell:" + cmd);
282 byteArrayOutputStream.writeBytes(stream.read());
283 } while (!stream.isClosed());
284 } catch (IOException e) {
285 String message = e.getMessage();
286 if (message != null && !message.equals("Stream closed")) {
290 return byteArrayOutputStream.toString(StandardCharsets.US_ASCII);
292 this.commandFuture = commandFuture;
293 return commandFuture.get(timeoutSec, TimeUnit.SECONDS);
296 private static AdbBase64 getBase64Impl() {
297 Charset asciiCharset = Charset.forName("ASCII");
298 return bytes -> new String(Base64.getEncoder().encode(bytes), asciiCharset);
301 private static AdbCrypto loadKeyPair(String pubKeyFile, String privKeyFile)
302 throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
303 File pub = new File(pubKeyFile);
304 File priv = new File(privKeyFile);
307 if (pub.exists() && priv.exists()) {
309 c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), priv, pub);
310 } catch (IOException ignored) {
316 c = AdbCrypto.generateAdbKeyPair(getBase64Impl());
317 c.saveAdbKeyPair(priv, pub);
322 public void disconnect() {
323 var commandFuture = this.commandFuture;
324 if (commandFuture != null && !commandFuture.isDone()) {
325 commandFuture.cancel(true);
327 var adb = connection;
332 } catch (IOException ignored) {
339 } catch (IOException ignored) {
345 public static class VolumeInfo {
350 VolumeInfo(int current, int min, int max) {
351 this.current = current;