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;
39 import java.util.stream.Collectors;
41 import org.eclipse.jdt.annotation.NonNullByDefault;
42 import org.eclipse.jdt.annotation.Nullable;
43 import org.openhab.core.OpenHAB;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
47 import com.tananaev.adblib.AdbBase64;
48 import com.tananaev.adblib.AdbConnection;
49 import com.tananaev.adblib.AdbCrypto;
50 import com.tananaev.adblib.AdbStream;
53 * The {@link AndroidDebugBridgeConfiguration} class encapsulates adb device connection logic.
55 * @author Miguel Álvarez - Initial contribution
58 public class AndroidDebugBridgeDevice {
59 public static final int ANDROID_MEDIA_STREAM = 3;
60 private static final String ADB_FOLDER = OpenHAB.getUserDataFolder() + File.separator + ".adb";
61 private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class);
62 private static final Pattern VOLUME_PATTERN = Pattern
63 .compile("volume is (?<current>\\d.*) in range \\[(?<min>\\d.*)\\.\\.(?<max>\\d.*)]");
64 private static final Pattern TAP_EVENT_PATTERN = Pattern.compile("(?<x>\\d+),(?<y>\\d+)");
65 private static final Pattern PACKAGE_NAME_PATTERN = Pattern
66 .compile("^([A-Za-z]{1}[A-Za-z\\d_]*\\.)+[A-Za-z][A-Za-z\\d_]*$");
67 private static final Pattern URL_PATTERN = Pattern.compile(
68 "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)$");
69 private static final Pattern INPUT_EVENT_PATTERN = Pattern
70 .compile("/(?<input>\\S+): (?<n1>\\S+) (?<n2>\\S+) (?<n3>\\S+)$", Pattern.MULTILINE);
71 private static final Pattern VERSION_PATTERN = Pattern
72 .compile("^(?<major>\\d+)(\\.)?(?<minor>\\d+)?(\\.)?(?<patch>\\*|\\d+)?");
73 private static final Pattern MAC_PATTERN = Pattern.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$");
75 private static final Pattern SECURE_SHELL_INPUT_PATTERN = Pattern.compile("^[^\\|\\&;\\\"]+$");
77 private static @Nullable AdbCrypto adbCrypto;
80 var logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class);
82 File directory = new File(ADB_FOLDER);
83 if (!directory.exists()) {
86 adbCrypto = loadKeyPair(ADB_FOLDER + File.separator + "adb_pub.key",
87 ADB_FOLDER + File.separator + "adb.key");
88 } catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) {
89 logger.warn("Unable to setup adb keys: {}", e.getMessage());
93 private final ScheduledExecutorService scheduler;
94 private final ReentrantLock commandLock = new ReentrantLock();
96 private String ip = "127.0.0.1";
97 private int port = 5555;
98 private int timeoutSec = 5;
99 private int recordDuration;
100 private @Nullable Socket socket;
101 private @Nullable AdbConnection connection;
102 private @Nullable Future<String> commandFuture;
103 private int majorVersionNumber = 0;
104 private int minorVersionNumber = 0;
105 private int patchVersionNumber = 0;
107 public AndroidDebugBridgeDevice(ScheduledExecutorService scheduler) {
108 this.scheduler = scheduler;
111 public void configure(String ip, int port, int timeout, int recordDuration) {
114 this.timeoutSec = timeout;
115 this.recordDuration = recordDuration;
118 public void sendKeyEvent(String eventCode)
119 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
120 runAdbShell("input", "keyevent", eventCode);
123 public void sendText(String text)
124 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
125 runAdbShell("input", "text", URLEncoder.encode(text, StandardCharsets.UTF_8));
128 public void sendTap(String point)
129 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
130 var match = TAP_EVENT_PATTERN.matcher(point);
131 if (!match.matches()) {
132 throw new AndroidDebugBridgeDeviceException("Unable to parse tap event");
134 runAdbShell("input", "mouse", "tap", match.group("x"), match.group("y"));
137 public void openUrl(String url)
138 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
139 var match = URL_PATTERN.matcher(url);
140 if (!match.matches()) {
141 throw new AndroidDebugBridgeDeviceException("Unable to parse url");
143 runAdbShell("am", "start", "-a", url);
146 public void startPackage(String packageName)
147 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
148 if (packageName.contains("/")) {
149 startPackageWithActivity(packageName);
152 if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
153 logger.warn("{} is not a valid package name", packageName);
156 var out = runAdbShell("monkey", "--pct-syskeys", "0", "-p", packageName, "-v", "1");
157 if (out.contains("monkey aborted")) {
158 startTVPackage(packageName);
162 private void startTVPackage(String packageName)
163 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
164 // https://developer.android.com/training/tv/start/start
165 String result = runAdbShell("monkey", "--pct-syskeys", "0", "-c", "android.intent.category.LEANBACK_LAUNCHER",
166 "-p", packageName, "1");
167 if (result.contains("monkey aborted")) {
168 throw new AndroidDebugBridgeDeviceException("Unable to open package");
172 public void startPackageWithActivity(String packageWithActivity)
173 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
174 var parts = packageWithActivity.split("/");
175 if (parts.length != 2) {
176 logger.warn("{} is not a valid package", packageWithActivity);
179 var packageName = parts[0];
180 var activityName = parts[1];
181 if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
182 logger.warn("{} is not a valid package name", packageName);
185 if (!SECURE_SHELL_INPUT_PATTERN.matcher(activityName).matches()) {
186 logger.warn("{} is not a valid activity name", activityName);
189 var out = runAdbShell("am", "start", "-n", packageWithActivity);
190 if (out.contains("usage: am")) {
191 out = runAdbShell("am", "start", packageWithActivity);
193 if (out.contains("usage: am") || out.contains("Exception")) {
194 logger.warn("open {} fail; retrying to open without activity info", packageWithActivity);
195 startPackage(packageName);
199 public void stopPackage(String packageName)
200 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
201 if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
202 logger.warn("{} is not a valid package name", packageName);
205 runAdbShell("am", "force-stop", packageName);
208 public String getCurrentPackage() throws AndroidDebugBridgeDeviceException, InterruptedException,
209 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
211 if (isAtLeastVersion(10)) {
212 out = runAdbShell("dumpsys", "window", "displays", "|", "grep", "mFocusedApp");
214 out = runAdbShell("dumpsys", "window", "windows", "|", "grep", "mFocusedApp");
216 var targetLine = Arrays.stream(out.split("\n")).findFirst().orElse("");
217 var lineParts = targetLine.split(" ");
218 if (lineParts.length >= 2) {
219 var packageActivityName = lineParts[lineParts.length - 2];
220 if (packageActivityName.contains("/")) {
221 return packageActivityName.split("/")[0];
224 throw new AndroidDebugBridgeDeviceReadException("Unable to read package name");
227 public boolean isAwake()
228 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
229 String devicesResp = runAdbShell("dumpsys", "activity", "|", "grep", "mWakefulness");
230 return devicesResp.contains("mWakefulness=Awake");
233 public boolean isScreenOn() throws InterruptedException, AndroidDebugBridgeDeviceException,
234 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
235 String devicesResp = runAdbShell("dumpsys", "power", "|", "grep", "'Display Power'");
236 if (devicesResp.contains("=")) {
238 var state = devicesResp.split("=")[1].trim();
239 return state.equals("ON");
240 } catch (NumberFormatException e) {
241 logger.debug("Unable to parse device screen state: {}", e.getMessage());
244 throw new AndroidDebugBridgeDeviceReadException("Unable to read screen state");
247 public boolean isPlayingMedia(String currentApp)
248 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
249 String devicesResp = runAdbShell("dumpsys", "media_session", "|", "grep", "-A", "100", "'Sessions Stack'", "|",
250 "grep", "-A", "50", currentApp);
251 String[] mediaSessions = devicesResp.split("\n\n");
252 if (mediaSessions.length == 0) {
253 // no media session found for current app
256 boolean isPlaying = mediaSessions[0].contains("PlaybackState {state=3");
257 logger.debug("device media state playing {}", isPlaying);
261 public boolean isPlayingAudio()
262 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
263 String audioDump = runAdbShell("dumpsys", "audio", "|", "grep", "ID:");
264 return audioDump.contains("state:started");
267 public VolumeInfo getMediaVolume() throws AndroidDebugBridgeDeviceException, InterruptedException,
268 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
269 return getVolume(ANDROID_MEDIA_STREAM);
272 public void setMediaVolume(int volume)
273 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
274 setVolume(ANDROID_MEDIA_STREAM, volume);
277 public int getPowerWakeLock() throws InterruptedException, AndroidDebugBridgeDeviceException,
278 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
279 String lockResp = runAdbShell("dumpsys", "power", "|", "grep", "Locks", "|", "grep", "'size='");
280 if (lockResp.contains("=")) {
282 return Integer.parseInt(lockResp.replace("\n", "").split("=")[1].trim());
283 } catch (NumberFormatException e) {
284 String message = String.format("Unable to parse device wake-lock '%s'", lockResp);
285 logger.debug("{}: {}", message, e.getMessage());
286 throw new AndroidDebugBridgeDeviceReadException(message);
289 throw new AndroidDebugBridgeDeviceReadException(String.format("Unable to read wake-lock '%s'", lockResp));
292 private void setVolume(int stream, int volume)
293 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
294 runAdbShell("media", "volume", "--show", "--stream", String.valueOf(stream), "--set", String.valueOf(volume));
297 public String getModel() throws AndroidDebugBridgeDeviceException, InterruptedException,
298 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
299 return getDeviceProp("ro.product.model");
302 public String getAndroidVersion() throws AndroidDebugBridgeDeviceException, InterruptedException,
303 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
304 return getDeviceProp("ro.build.version.release");
307 public void setAndroidVersion(String version) {
308 var matcher = VERSION_PATTERN.matcher(version);
309 if (!matcher.find()) {
310 logger.warn("Unable to parse android version");
313 this.majorVersionNumber = Integer.parseInt(matcher.group("major"));
314 var minorMatch = matcher.group("minor");
315 var patchMatch = matcher.group("patch");
316 this.minorVersionNumber = minorMatch != null ? Integer.parseInt(minorMatch) : 0;
317 this.patchVersionNumber = patchMatch != null ? Integer.parseInt(patchMatch) : 0;
320 public String getBrand() throws AndroidDebugBridgeDeviceException, InterruptedException,
321 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
322 return getDeviceProp("ro.product.brand");
325 public String getSerialNo() throws AndroidDebugBridgeDeviceException, InterruptedException,
326 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
327 return getDeviceProp("ro.serialno");
330 public String getMacAddress() throws AndroidDebugBridgeDeviceException, InterruptedException,
331 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
332 var macAddress = runAdbShell("cat", "/sys/class/net/wlan0/address").replace("\n", "").replace("\r", "");
333 var matcher = MAC_PATTERN.matcher(macAddress);
334 if (!matcher.find()) {
335 macAddress = runAdbShell("ip", "address", "|", "grep", "-m", "1", "link/ether", "|", "awk", "'{print $2}'")
336 .replace("\n", "").replace("\r", "");
337 matcher = MAC_PATTERN.matcher(macAddress);
338 if (matcher.find()) {
342 return "00:00:00:00:00:00";
345 private String getDeviceProp(String name) throws AndroidDebugBridgeDeviceException, InterruptedException,
346 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
347 var propValue = runAdbShell("getprop", name, "&&", "sleep", "0.3").replace("\n", "").replace("\r", "");
348 if (propValue.length() == 0) {
349 throw new AndroidDebugBridgeDeviceReadException(String.format("Unable to get device property '%s'", name));
354 private VolumeInfo getVolume(int stream) throws AndroidDebugBridgeDeviceException, InterruptedException,
355 AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
356 String volumeResp = runAdbShell("media", "volume", "--show", "--stream", String.valueOf(stream), "--get", "|",
358 Matcher matcher = VOLUME_PATTERN.matcher(volumeResp);
359 if (!matcher.find()) {
360 throw new AndroidDebugBridgeDeviceReadException("Unable to get volume info");
362 var volumeInfo = new VolumeInfo(Integer.parseInt(matcher.group("current")),
363 Integer.parseInt(matcher.group("min")), Integer.parseInt(matcher.group("max")));
364 logger.debug("Device {}:{} VolumeInfo: current {}, min {}, max {}", this.ip, this.port, volumeInfo.current,
365 volumeInfo.min, volumeInfo.max);
369 public String recordInputEvents()
370 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
371 String out = runAdbShell(recordDuration * 2, "getevent", "&", "sleep", Integer.toString(recordDuration), "&&",
373 var matcher = INPUT_EVENT_PATTERN.matcher(out);
374 var commandList = new ArrayList<String>();
376 while (matcher.find()) {
377 String inputPath = matcher.group("input");
378 int n1 = Integer.parseInt(matcher.group("n1"), 16);
379 int n2 = Integer.parseInt(matcher.group("n2"), 16);
380 int n3 = Integer.parseInt(matcher.group("n3"), 16);
381 commandList.add(String.format("sendevent /%s %d %d %d", inputPath, n1, n2, n3));
383 } catch (NumberFormatException e) {
384 logger.warn("NumberFormatException while parsing events, aborting");
387 return String.join(" && ", commandList);
390 public void sendInputEvents(String command)
391 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
392 String out = runAdbShell(command.split(" "));
393 if (out.length() != 0) {
394 logger.warn("Device event unexpected output: {}", out);
395 throw new AndroidDebugBridgeDeviceException("Device event execution fail");
399 public void rebootDevice()
400 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
402 runAdbShell("reboot", "&", "sleep", "0.1", "&&", "exit");
408 public void powerOffDevice()
409 throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
411 runAdbShell("reboot", "-p", "&", "sleep", "0.1", "&&", "exit");
417 public void startIntent(String command)
418 throws AndroidDebugBridgeDeviceException, ExecutionException, InterruptedException, TimeoutException {
419 String[] commandParts = command.split("\\|\\|");
420 if (commandParts.length == 0) {
421 throw new AndroidDebugBridgeDeviceException("Empty command");
423 String targetPackage = commandParts[0];
424 var targetPackageParts = targetPackage.split("/");
425 if (targetPackageParts.length > 2) {
426 throw new AndroidDebugBridgeDeviceException("Invalid target package " + targetPackage);
428 if (!PACKAGE_NAME_PATTERN.matcher(targetPackageParts[0]).matches()) {
429 logger.warn("{} is not a valid package name", targetPackageParts[0]);
432 if (targetPackageParts.length == 2 && !SECURE_SHELL_INPUT_PATTERN.matcher(targetPackageParts[1]).matches()) {
433 logger.warn("{} is not a valid activity name", targetPackageParts[1]);
437 String action = null;
439 String dataUri = null;
441 String mimeType = null;
443 String category = null;
445 String component = null;
448 Map<String, Boolean> extraBooleans = new HashMap<>();
449 Map<String, String> extraStrings = new HashMap<>();
450 Map<String, Integer> extraIntegers = new HashMap<>();
451 Map<String, Float> extraFloats = new HashMap<>();
452 Map<String, Long> extraLongs = new HashMap<>();
453 Map<String, URI> extraUris = new HashMap<>();
454 for (var i = 1; i < commandParts.length; i++) {
455 var commandPart = commandParts[i];
456 var endToken = commandPart.indexOf(">");
457 var argName = commandPart.substring(0, endToken + 1);
458 var argValue = commandPart.substring(endToken + 1);
464 if (!PACKAGE_NAME_PATTERN.matcher(argValue).matches()) {
465 logger.warn("{} is not a valid action name", argValue);
472 if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
473 logger.warn("{}, insecure input value", argValue);
480 if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
481 logger.warn("{}, insecure input value", argValue);
488 if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
489 logger.warn("{}, insecure input value", argValue);
496 if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
497 logger.warn("{}, insecure input value", argValue);
500 component = argValue;
504 if (!SECURE_SHELL_INPUT_PATTERN.matcher(argValue).matches()) {
505 logger.warn("{}, insecure input value", argValue);
512 valueParts = argValue.split(" ");
513 if (valueParts.length != 2) {
514 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
518 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
519 logger.warn("{}, insecure input value", valueParts[0]);
522 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
523 logger.warn("{}, insecure input value", valueParts[1]);
526 extraStrings.put(valueParts[0], valueParts[1]);
529 valueParts = argValue.split(" ");
530 if (valueParts.length != 2) {
531 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
535 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
536 logger.warn("{}, insecure input value", valueParts[0]);
539 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
540 logger.warn("{}, insecure input value", valueParts[1]);
543 extraBooleans.put(valueParts[0], Boolean.parseBoolean(valueParts[1]));
546 valueParts = argValue.split(" ");
547 if (valueParts.length != 2) {
548 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
552 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
553 logger.warn("{}, insecure input value", valueParts[0]);
556 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
557 logger.warn("{}, insecure input value", valueParts[1]);
561 extraIntegers.put(valueParts[0], Integer.parseInt(valueParts[1]));
562 } catch (NumberFormatException e) {
563 logger.warn("Unable to parse {} as integer", valueParts[1]);
568 valueParts = argValue.split(" ");
569 if (valueParts.length != 2) {
570 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
574 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
575 logger.warn("{}, insecure input value", valueParts[0]);
578 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
579 logger.warn("{}, insecure input value", valueParts[1]);
583 extraLongs.put(valueParts[0], Long.parseLong(valueParts[1]));
584 } catch (NumberFormatException e) {
585 logger.warn("Unable to parse {} as long", valueParts[1]);
590 valueParts = argValue.split(" ");
591 if (valueParts.length != 2) {
592 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
596 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
597 logger.warn("{}, insecure input value", valueParts[0]);
600 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
601 logger.warn("{}, insecure input value", valueParts[1]);
605 extraFloats.put(valueParts[0], Float.parseFloat(valueParts[1]));
606 } catch (NumberFormatException e) {
607 logger.warn("Unable to parse {} as float", valueParts[1]);
612 valueParts = argValue.split(" ");
613 if (valueParts.length != 2) {
614 logger.warn("argument '{}' requires a key value pair separated by space, current value '{}'",
618 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[0]).matches()) {
619 logger.warn("{}, insecure input value", valueParts[0]);
622 if (!SECURE_SHELL_INPUT_PATTERN.matcher(valueParts[1]).matches()) {
623 logger.warn("{}, insecure input value", valueParts[1]);
626 extraUris.put(valueParts[0], URI.create(valueParts[1]));
629 throw new AndroidDebugBridgeDeviceException("Unsupported arg " + argName
630 + ". Open an issue or pr for it if you think support should be added.");
634 StringBuilder adbCommandBuilder = new StringBuilder("am start -n " + targetPackage);
635 if (action != null) {
636 adbCommandBuilder.append(" -a ").append(action);
638 if (dataUri != null) {
639 adbCommandBuilder.append(" -d ").append(dataUri);
641 if (mimeType != null) {
642 adbCommandBuilder.append(" -t ").append(mimeType);
644 if (category != null) {
645 adbCommandBuilder.append(" -c ").append(category);
647 if (component != null) {
648 adbCommandBuilder.append(" -n ").append(component);
651 adbCommandBuilder.append(" -f ").append(flags);
653 if (!extraStrings.isEmpty()) {
654 adbCommandBuilder.append(extraStrings.entrySet().stream()
655 .map(entry -> " --es \"" + entry.getKey() + "\" \"" + entry.getValue() + "\"")
656 .collect(Collectors.joining(" ")));
658 if (!extraBooleans.isEmpty()) {
659 adbCommandBuilder.append(extraBooleans.entrySet().stream()
660 .map(entry -> " --ez \"" + entry.getKey() + "\" " + entry.getValue())
661 .collect(Collectors.joining(" ")));
663 if (!extraIntegers.isEmpty()) {
664 adbCommandBuilder.append(extraIntegers.entrySet().stream()
665 .map(entry -> " --ei \"" + entry.getKey() + "\" " + entry.getValue())
666 .collect(Collectors.joining(" ")));
668 if (!extraFloats.isEmpty()) {
669 adbCommandBuilder.append(
670 extraFloats.entrySet().stream().map(entry -> " --ef \"" + entry.getKey() + "\" " + entry.getValue())
671 .collect(Collectors.joining(" ")));
673 if (!extraLongs.isEmpty()) {
674 adbCommandBuilder.append(
675 extraLongs.entrySet().stream().map(entry -> " --el \"" + entry.getKey() + "\" " + entry.getValue())
676 .collect(Collectors.joining(" ")));
678 runAdbShell(adbCommandBuilder.toString());
681 public boolean isConnected() {
682 var currentSocket = socket;
683 return currentSocket != null && currentSocket.isConnected();
686 public void connect() throws AndroidDebugBridgeDeviceException, InterruptedException {
688 AdbConnection adbConnection;
690 AdbCrypto crypto = adbCrypto;
691 if (crypto == null) {
692 throw new AndroidDebugBridgeDeviceException("Device not connected");
697 sock.connect(new InetSocketAddress(ip, port), (int) TimeUnit.SECONDS.toMillis(15));
698 } catch (IOException e) {
699 logger.debug("Error connecting to {}: [{}] {}", ip, e.getClass().getName(), e.getMessage());
700 if ("Socket closed".equals(e.getMessage())) {
701 // Connection aborted by us
702 throw new InterruptedException();
704 throw new AndroidDebugBridgeDeviceException("Can not open socket " + ip + ":" + port);
707 adbConnection = AdbConnection.create(sock, crypto);
708 connection = adbConnection;
709 adbConnection.connect(15, TimeUnit.SECONDS, false);
710 } catch (IOException e) {
711 logger.debug("Error connecting to {}: {}", ip, e.getMessage());
712 throw new AndroidDebugBridgeDeviceException("Can not open adb connection " + ip + ":" + port);
716 private String runAdbShell(String... args)
717 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
718 return runAdbShell(timeoutSec, args);
721 private String runAdbShell(int commandTimeout, String... args)
722 throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
723 var adb = connection;
725 throw new AndroidDebugBridgeDeviceException("Device not connected");
729 var commandFuture = scheduler.submit(() -> {
730 var byteArrayOutputStream = new ByteArrayOutputStream();
731 String cmd = String.join(" ", args);
732 logger.debug("{} - shell:{}", ip, cmd);
733 try (AdbStream stream = adb.open("shell:" + cmd)) {
735 byteArrayOutputStream.writeBytes(stream.read());
736 } while (!stream.isClosed());
737 } catch (IOException e) {
738 if (!"Stream closed".equals(e.getMessage())) {
742 return byteArrayOutputStream.toString(StandardCharsets.US_ASCII);
744 this.commandFuture = commandFuture;
745 return commandFuture.get(commandTimeout, TimeUnit.SECONDS);
747 var commandFuture = this.commandFuture;
748 if (commandFuture != null) {
749 commandFuture.cancel(true);
750 this.commandFuture = null;
752 commandLock.unlock();
756 private static AdbBase64 getBase64Impl() {
757 Charset asciiCharset = Charset.forName("ASCII");
758 return bytes -> new String(Base64.getEncoder().encode(bytes), asciiCharset);
761 private static AdbCrypto loadKeyPair(String pubKeyFile, String privKeyFile)
762 throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
763 File pub = new File(pubKeyFile);
764 File priv = new File(privKeyFile);
767 if (pub.exists() && priv.exists()) {
769 c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), priv, pub);
770 } catch (IOException ignored) {
776 c = AdbCrypto.generateAdbKeyPair(getBase64Impl());
777 c.saveAdbKeyPair(priv, pub);
782 public void disconnect() {
783 var commandFuture = this.commandFuture;
784 if (commandFuture != null && !commandFuture.isDone()) {
785 commandFuture.cancel(true);
787 var adb = connection;
792 } catch (IOException ignored) {
799 } catch (IOException ignored) {
805 private boolean isAtLeastVersion(int major) {
806 return isAtLeastVersion(major, 0);
809 private boolean isAtLeastVersion(int major, int minor) {
810 return isAtLeastVersion(major, minor, 0);
813 private boolean isAtLeastVersion(int major, int minor, int patch) {
814 return majorVersionNumber > major || (majorVersionNumber == major
815 && (minorVersionNumber > minor || (minorVersionNumber == minor && patchVersionNumber >= patch)));
818 public static class VolumeInfo {
823 VolumeInfo(int current, int min, int max) {
824 this.current = current;