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 java.io.ByteArrayOutputStream;
17 import java.io.IOException;
18 import java.net.InetSocketAddress;
19 import java.net.Socket;
21 import java.net.URLEncoder;
22 import java.nio.charset.Charset;
23 import java.nio.charset.StandardCharsets;
24 import java.security.NoSuchAlgorithmException;
25 import java.security.spec.InvalidKeySpecException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Base64;
29 import java.util.HashMap;
31 import java.util.concurrent.ExecutionException;
32 import java.util.concurrent.Future;
33 import java.util.concurrent.ScheduledExecutorService;
34 import java.util.concurrent.TimeUnit;
35 import java.util.concurrent.TimeoutException;
36 import java.util.concurrent.locks.ReentrantLock;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
40 import org.eclipse.jdt.annotation.NonNullByDefault;
41 import org.eclipse.jdt.annotation.Nullable;
42 import org.openhab.core.OpenHAB;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
46 import com.tananaev.adblib.AdbBase64;
47 import com.tananaev.adblib.AdbConnection;
48 import com.tananaev.adblib.AdbCrypto;
49 import com.tananaev.adblib.AdbStream;
52 * The {@link AndroidDebugBridgeConfiguration} class encapsulates adb device connection logic.
54 * @author Miguel Álvarez - Initial contribution
57 public class AndroidDebugBridgeDevice {
58 public static final int ANDROID_MEDIA_STREAM = 3;
59 private static final String ADB_FOLDER = OpenHAB.getUserDataFolder() + File.separator + ".adb";
60 private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class);
61 private static final Pattern VOLUME_PATTERN = Pattern
62 .compile("volume is (?<current>\\d.*) in range \\[(?<min>\\d.*)\\.\\.(?<max>\\d.*)]");
63 private static final Pattern TAP_EVENT_PATTERN = Pattern.compile("(?<x>\\d+),(?<y>\\d+)");
64 private static final Pattern PACKAGE_NAME_PATTERN = Pattern
65 .compile("^([A-Za-z]{1}[A-Za-z\\d_]*\\.)+[A-Za-z][A-Za-z\\d_]*$");
66 private static final Pattern URL_PATTERN = Pattern.compile(
67 "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)$");
68 private static final Pattern INPUT_EVENT_PATTERN = Pattern
69 .compile("/(?<input>\\S+): (?<n1>\\S+) (?<n2>\\S+) (?<n3>\\S+)$", Pattern.MULTILINE);
71 private static final Pattern SECURE_SHELL_INPUT_PATTERN = Pattern.compile("^[^\\|\\&;\\\"]+$");
73 private static @Nullable AdbCrypto adbCrypto;
76 var logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class);
78 File directory = new File(ADB_FOLDER);
79 if (!directory.exists()) {
82 adbCrypto = loadKeyPair(ADB_FOLDER + File.separator + "adb_pub.key",
83 ADB_FOLDER + File.separator + "adb.key");
84 } catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) {
85 logger.warn("Unable to setup adb keys: {}", e.getMessage());
89 private final ScheduledExecutorService scheduler;
90 private final ReentrantLock commandLock = new ReentrantLock();
92 private String ip = "127.0.0.1";
93 private int port = 5555;
94 private int timeoutSec = 5;
95 private int recordDuration;
96 private @Nullable Socket socket;
97 private @Nullable AdbConnection connection;
98 private @Nullable Future<String> commandFuture;
100 public AndroidDebugBridgeDevice(ScheduledExecutorService scheduler) {
101 this.scheduler = scheduler;
104 public void configure(String ip, int port, int timeout, int recordDuration) {
107 this.timeoutSec = timeout;
108 this.recordDuration = recordDuration;
111 public void sendKeyEvent(String eventCode)
112 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
113 runAdbShell("input", "keyevent", eventCode);
116 public void sendText(String text)
117 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
118 runAdbShell("input", "text", URLEncoder.encode(text, StandardCharsets.UTF_8));
121 public void sendTap(String point)
122 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
123 var match = TAP_EVENT_PATTERN.matcher(point);
124 if (!match.matches()) {
125 throw new AndroidDebugBridgeDeviceException("Unable to parse tap event");
127 runAdbShell("input", "mouse", "tap", match.group("x"), match.group("y"));
130 public void openUrl(String url)
131 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
132 var match = URL_PATTERN.matcher(url);
133 if (!match.matches()) {
134 throw new AndroidDebugBridgeDeviceException("Unable to parse url");
136 runAdbShell("am", "start", "-a", url);
139 public void startPackage(String packageName)
140 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
141 if (packageName.contains("/")) {
142 startPackageWithActivity(packageName);
145 if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
146 logger.warn("{} is not a valid package name", packageName);
149 var out = runAdbShell("monkey", "--pct-syskeys", "0", "-p", packageName, "-v", "1");
150 if (out.contains("monkey aborted")) {
151 startTVPackage(packageName);
155 private void startTVPackage(String packageName)
156 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
157 // https://developer.android.com/training/tv/start/start
158 String result = runAdbShell("monkey", "--pct-syskeys", "0", "-c", "android.intent.category.LEANBACK_LAUNCHER",
159 "-p", packageName, "1");
160 if (result.contains("monkey aborted")) {
161 throw new AndroidDebugBridgeDeviceException("Unable to open package");
165 public void startPackageWithActivity(String packageWithActivity)
166 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
167 var parts = packageWithActivity.split("/");
168 if (parts.length != 2) {
169 logger.warn("{} is not a valid package", packageWithActivity);
172 var packageName = parts[0];
173 var activityName = parts[1];
174 if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
175 logger.warn("{} is not a valid package name", packageName);
178 if (!SECURE_SHELL_INPUT_PATTERN.matcher(activityName).matches()) {
179 logger.warn("{} is not a valid activity name", activityName);
182 var out = runAdbShell("am", "start", "-n", packageWithActivity);
183 if (out.contains("usage: am")) {
184 out = runAdbShell("am", "start", packageWithActivity);
186 if (out.contains("usage: am") || out.contains("Exception")) {
187 logger.warn("open {} fail; retrying to open without activity info", packageWithActivity);
188 startPackage(packageName);
192 public void stopPackage(String packageName)
193 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
194 if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
195 logger.warn("{} is not a valid package name", packageName);
198 runAdbShell("am", "force-stop", packageName);
201 public String getCurrentPackage() throws AndroidDebugBridgeDeviceException, InterruptedException,
202 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
203 var out = runAdbShell("dumpsys", "window", "windows", "|", "grep", "mFocusedApp");
204 var targetLine = Arrays.stream(out.split("\n")).findFirst().orElse("");
205 var lineParts = targetLine.split(" ");
206 if (lineParts.length >= 2) {
207 var packageActivityName = lineParts[lineParts.length - 2];
208 if (packageActivityName.contains("/")) {
209 return packageActivityName.split("/")[0];
212 throw new AndroidDebugBridgeDeviceReadException("Unable to read package name");
215 public boolean isAwake()
216 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
217 String devicesResp = runAdbShell("dumpsys", "activity", "|", "grep", "mWakefulness");
218 return devicesResp.contains("mWakefulness=Awake");
221 public boolean isScreenOn() throws InterruptedException, AndroidDebugBridgeDeviceException,
222 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
223 String devicesResp = runAdbShell("dumpsys", "power", "|", "grep", "'Display Power'");
224 if (devicesResp.contains("=")) {
226 var state = devicesResp.split("=")[1].trim();
227 return state.equals("ON");
228 } catch (NumberFormatException e) {
229 logger.debug("Unable to parse device screen state: {}", e.getMessage());
232 throw new AndroidDebugBridgeDeviceReadException("Unable to read screen state");
235 public boolean isPlayingMedia(String currentApp)
236 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
237 String devicesResp = runAdbShell("dumpsys", "media_session", "|", "grep", "-A", "100", "'Sessions Stack'", "|",
238 "grep", "-A", "50", currentApp);
239 String[] mediaSessions = devicesResp.split("\n\n");
240 if (mediaSessions.length == 0) {
241 // no media session found for current app
244 boolean isPlaying = mediaSessions[0].contains("PlaybackState {state=3");
245 logger.debug("device media state playing {}", isPlaying);
249 public boolean isPlayingAudio()
250 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
251 String audioDump = runAdbShell("dumpsys", "audio", "|", "grep", "ID:");
252 return audioDump.contains("state:started");
255 public VolumeInfo getMediaVolume() throws AndroidDebugBridgeDeviceException, InterruptedException,
256 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
257 return getVolume(ANDROID_MEDIA_STREAM);
260 public void setMediaVolume(int volume)
261 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
262 setVolume(ANDROID_MEDIA_STREAM, volume);
265 public int getPowerWakeLock() throws InterruptedException, AndroidDebugBridgeDeviceException,
266 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
267 String lockResp = runAdbShell("dumpsys", "power", "|", "grep", "Locks", "|", "grep", "'size='");
268 if (lockResp.contains("=")) {
270 return Integer.parseInt(lockResp.replace("\n", "").split("=")[1].trim());
271 } catch (NumberFormatException e) {
272 String message = String.format("Unable to parse device wake-lock '%s'", lockResp);
273 logger.debug("{}: {}", message, e.getMessage());
274 throw new AndroidDebugBridgeDeviceReadException(message);
277 throw new AndroidDebugBridgeDeviceReadException(String.format("Unable to read wake-lock '%s'", lockResp));
280 private void setVolume(int stream, int volume)
281 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
282 runAdbShell("media", "volume", "--show", "--stream", String.valueOf(stream), "--set", String.valueOf(volume));
285 public String getModel() throws AndroidDebugBridgeDeviceException, InterruptedException,
286 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
287 return getDeviceProp("ro.product.model");
290 public String getAndroidVersion() throws AndroidDebugBridgeDeviceException, InterruptedException,
291 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
292 return getDeviceProp("ro.build.version.release");
295 public String getBrand() throws AndroidDebugBridgeDeviceException, InterruptedException,
296 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
297 return getDeviceProp("ro.product.brand");
300 public String getSerialNo() throws AndroidDebugBridgeDeviceException, InterruptedException,
301 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
302 return getDeviceProp("ro.serialno");
305 public String getMacAddress() throws AndroidDebugBridgeDeviceException, InterruptedException,
306 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
307 return runAdbShell("cat", "/sys/class/net/wlan0/address").replace("\n", "").replace("\r", "");
310 private String getDeviceProp(String name) throws AndroidDebugBridgeDeviceException, InterruptedException,
311 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
312 var propValue = runAdbShell("getprop", name, "&&", "sleep", "0.3").replace("\n", "").replace("\r", "");
313 if (propValue.length() == 0) {
314 throw new AndroidDebugBridgeDeviceReadException(String.format("Unable to get device property '%s'", name));
319 private VolumeInfo getVolume(int stream) throws AndroidDebugBridgeDeviceException, InterruptedException,
320 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
321 String volumeResp = runAdbShell("media", "volume", "--show", "--stream", String.valueOf(stream), "--get", "|",
323 Matcher matcher = VOLUME_PATTERN.matcher(volumeResp);
324 if (!matcher.find()) {
325 throw new AndroidDebugBridgeDeviceReadException("Unable to get volume info");
327 var volumeInfo = new VolumeInfo(Integer.parseInt(matcher.group("current")),
328 Integer.parseInt(matcher.group("min")), Integer.parseInt(matcher.group("max")));
329 logger.debug("Device {}:{} VolumeInfo: current {}, min {}, max {}", this.ip, this.port, volumeInfo.current,
330 volumeInfo.min, volumeInfo.max);
334 public String recordInputEvents()
335 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
336 String out = runAdbShell(recordDuration * 2, "getevent", "&", "sleep", Integer.toString(recordDuration), "&&",
338 var matcher = INPUT_EVENT_PATTERN.matcher(out);
339 var commandList = new ArrayList<String>();
341 while (matcher.find()) {
342 String inputPath = matcher.group("input");
343 int n1 = Integer.parseInt(matcher.group("n1"), 16);
344 int n2 = Integer.parseInt(matcher.group("n2"), 16);
345 int n3 = Integer.parseInt(matcher.group("n3"), 16);
346 commandList.add(String.format("sendevent /%s %d %d %d", inputPath, n1, n2, n3));
348 } catch (NumberFormatException e) {
349 logger.warn("NumberFormatException while parsing events, aborting");
352 return String.join(" && ", commandList);
355 public void sendInputEvents(String command)
356 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
357 String out = runAdbShell(command.split(" "));
358 if (out.length() != 0) {
359 logger.warn("Device event unexpected output: {}", out);
360 throw new AndroidDebugBridgeDeviceException("Device event execution fail");
364 public void rebootDevice()
365 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
367 runAdbShell("reboot", "&", "sleep", "0.1", "&&", "exit");
373 public void powerOffDevice()
374 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
376 runAdbShell("reboot", "-p", "&", "sleep", "0.1", "&&", "exit");
382 public void startIntent(String command)
383 throws AndroidDebugBridgeDeviceException, ExecutionException, InterruptedException, TimeoutException {
384 String[] commandParts = command.split("\\|\\|");
385 if (commandParts.length == 0) {
386 throw new AndroidDebugBridgeDeviceException("Empty command");
388 String targetPackage = commandParts[0];
389 var targetPackageParts = targetPackage.split("/");
390 if (targetPackageParts.length > 2) {
391 throw new AndroidDebugBridgeDeviceException("Invalid target package " + targetPackage);
393 if (!PACKAGE_NAME_PATTERN.matcher(targetPackageParts[0]).matches()) {
394 logger.warn("{} is not a valid package name", targetPackageParts[0]);
397 if (targetPackageParts.length == 2 && !SECURE_SHELL_INPUT_PATTERN.matcher(targetPackageParts[1]).matches()) {
398 logger.warn("{} is not a valid activity name", targetPackageParts[1]);
402 String action = null;
404 String dataUri = null;
406 String mimeType = null;
408 String category = null;
410 String component = null;
413 Map<String, Boolean> extraBooleans = new HashMap<>();
414 Map<String, String> extraStrings = new HashMap<>();
415 Map<String, Integer> extraIntegers = new HashMap<>();
416 Map<String, Float> extraFloats = new HashMap<>();
417 Map<String, Long> extraLongs = new HashMap<>();
418 Map<String, URI> extraUris = new HashMap<>();
419 for (var i = 1; i < commandParts.length - 1; i++) {
420 var commandPart = commandParts[i];
421 var endToken = commandPart.indexOf(">");
422 var argName = commandPart.substring(0, endToken + 1);
423 var argValue = commandPart.substring(endToken + 1);
429 if (!PACKAGE_NAME_PATTERN.matcher(argValue).matches()) {
430 logger.warn("{} is not a valid action name", argValue);
437 if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
438 logger.warn("{}, insecure input value", argValue);
445 if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
446 logger.warn("{}, insecure input value", argValue);
453 if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
454 logger.warn("{}, insecure input value", argValue);
461 if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
462 logger.warn("{}, insecure input value", argValue);
465 component = argValue;
469 if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
470 logger.warn("{}, insecure input value", argValue);
477 valueParts = argValue.split(" ");
478 if (valueParts.length != 2) {
479 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
483 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
484 logger.warn("{}, insecure input value", valueParts[0]);
487 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
488 logger.warn("{}, insecure input value", valueParts[1]);
491 extraStrings.put(valueParts[0], valueParts[1]);
494 valueParts = argValue.split(" ");
495 if (valueParts.length != 2) {
496 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
500 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
501 logger.warn("{}, insecure input value", valueParts[0]);
504 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
505 logger.warn("{}, insecure input value", valueParts[1]);
508 extraBooleans.put(valueParts[0], Boolean.parseBoolean(valueParts[1]));
511 valueParts = argValue.split(" ");
512 if (valueParts.length != 2) {
513 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
517 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
518 logger.warn("{}, insecure input value", valueParts[0]);
521 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
522 logger.warn("{}, insecure input value", valueParts[1]);
526 extraIntegers.put(valueParts[0], Integer.parseInt(valueParts[1]));
527 } catch (NumberFormatException e) {
528 logger.warn("Unable to parse {} as integer", valueParts[1]);
533 valueParts = argValue.split(" ");
534 if (valueParts.length != 2) {
535 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
539 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
540 logger.warn("{}, insecure input value", valueParts[0]);
543 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
544 logger.warn("{}, insecure input value", valueParts[1]);
548 extraLongs.put(valueParts[0], Long.parseLong(valueParts[1]));
549 } catch (NumberFormatException e) {
550 logger.warn("Unable to parse {} as long", valueParts[1]);
555 valueParts = argValue.split(" ");
556 if (valueParts.length != 2) {
557 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
561 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
562 logger.warn("{}, insecure input value", valueParts[0]);
565 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
566 logger.warn("{}, insecure input value", valueParts[1]);
570 extraFloats.put(valueParts[0], Float.parseFloat(valueParts[1]));
571 } catch (NumberFormatException e) {
572 logger.warn("Unable to parse {} as float", valueParts[1]);
577 valueParts = argValue.split(" ");
578 if (valueParts.length != 2) {
579 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
583 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
584 logger.warn("{}, insecure input value", valueParts[0]);
587 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
588 logger.warn("{}, insecure input value", valueParts[1]);
591 extraUris.put(valueParts[0], URI.create(valueParts[1]));
594 throw new AndroidDebugBridgeDeviceException("Unsupported arg " + argName
595 + ". Open an issue or pr for it if you think support should be added.");
599 StringBuilder adbCommandBuilder = new StringBuilder("am start " + targetPackage);
600 if (action != null) {
601 adbCommandBuilder.append(" -a ").append(action);
603 if (dataUri != null) {
604 adbCommandBuilder.append(" -d ").append(dataUri);
606 if (mimeType != null) {
607 adbCommandBuilder.append(" -t ").append(mimeType);
609 if (category != null) {
610 adbCommandBuilder.append(" -c ").append(category);
612 if (component != null) {
613 adbCommandBuilder.append(" -n ").append(component);
616 adbCommandBuilder.append(" -f ").append(flags);
618 if (!extraStrings.isEmpty()) {
619 adbCommandBuilder.append(extraStrings.entrySet().stream()
620 .map(entry -> " --es \"" + entry.getKey() + "\" \"" + entry.getValue() + "\""));
622 if (!extraBooleans.isEmpty()) {
623 adbCommandBuilder.append(extraBooleans.entrySet().stream()
624 .map(entry -> " --ez \"" + entry.getKey() + "\" " + entry.getValue()));
626 if (!extraIntegers.isEmpty()) {
627 adbCommandBuilder.append(extraIntegers.entrySet().stream()
628 .map(entry -> " --ei \"" + entry.getKey() + "\" " + entry.getValue()));
630 if (!extraFloats.isEmpty()) {
631 adbCommandBuilder.append(extraFloats.entrySet().stream()
632 .map(entry -> " --ef \"" + entry.getKey() + "\" " + entry.getValue()));
634 if (!extraLongs.isEmpty()) {
635 adbCommandBuilder.append(extraLongs.entrySet().stream()
636 .map(entry -> " --el \"" + entry.getKey() + "\" " + entry.getValue()));
638 runAdbShell(adbCommandBuilder.toString());
641 public boolean isConnected() {
642 var currentSocket = socket;
643 return currentSocket != null && currentSocket.isConnected();
646 public void connect() throws AndroidDebugBridgeDeviceException, InterruptedException {
648 AdbConnection adbConnection;
650 AdbCrypto crypto = adbCrypto;
651 if (crypto == null) {
652 throw new AndroidDebugBridgeDeviceException("Device not connected");
657 sock.connect(new InetSocketAddress(ip, port), (int) TimeUnit.SECONDS.toMillis(15));
658 } catch (IOException e) {
659 logger.debug("Error connecting to {}: [{}] {}", ip, e.getClass().getName(), e.getMessage());
660 if ("Socket closed".equals(e.getMessage())) {
661 // Connection aborted by us
662 throw new InterruptedException();
664 throw new AndroidDebugBridgeDeviceException("Can not open socket " + ip + ":" + port);
667 adbConnection = AdbConnection.create(sock, crypto);
668 connection = adbConnection;
669 adbConnection.connect(15, TimeUnit.SECONDS, false);
670 } catch (IOException e) {
671 logger.debug("Error connecting to {}: {}", ip, e.getMessage());
672 throw new AndroidDebugBridgeDeviceException("Can not open adb connection " + ip + ":" + port);
676 private String runAdbShell(String... args)
677 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
678 return runAdbShell(timeoutSec, args);
681 private String runAdbShell(int commandTimeout, String... args)
682 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
683 var adb = connection;
685 throw new AndroidDebugBridgeDeviceException("Device not connected");
689 var commandFuture = scheduler.submit(() -> {
690 var byteArrayOutputStream = new ByteArrayOutputStream();
691 String cmd = String.join(" ", args);
692 logger.debug("{} - shell:{}", ip, cmd);
693 try (AdbStream stream = adb.open("shell:" + cmd)) {
695 byteArrayOutputStream.writeBytes(stream.read());
696 } while (!stream.isClosed());
697 } catch (IOException e) {
698 if (!"Stream closed".equals(e.getMessage())) {
702 return byteArrayOutputStream.toString(StandardCharsets.US_ASCII);
704 this.commandFuture = commandFuture;
705 return commandFuture.get(commandTimeout, TimeUnit.SECONDS);
707 var commandFuture = this.commandFuture;
708 if (commandFuture != null) {
709 commandFuture.cancel(true);
710 this.commandFuture = null;
712 commandLock.unlock();
716 private static AdbBase64 getBase64Impl() {
717 Charset asciiCharset = Charset.forName("ASCII");
718 return bytes -> new String(Base64.getEncoder().encode(bytes), asciiCharset);
721 private static AdbCrypto loadKeyPair(String pubKeyFile, String privKeyFile)
722 throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
723 File pub = new File(pubKeyFile);
724 File priv = new File(privKeyFile);
727 if (pub.exists() && priv.exists()) {
729 c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), priv, pub);
730 } catch (IOException ignored) {
736 c = AdbCrypto.generateAdbKeyPair(getBase64Impl());
737 c.saveAdbKeyPair(priv, pub);
742 public void disconnect() {
743 var commandFuture = this.commandFuture;
744 if (commandFuture != null && !commandFuture.isDone()) {
745 commandFuture.cancel(true);
747 var adb = connection;
752 } catch (IOException ignored) {
759 } catch (IOException ignored) {
765 public static class VolumeInfo {
770 VolumeInfo(int current, int min, int max) {
771 this.current = current;