├── .github
└── workflows
│ └── gradle.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
└── java
│ └── com
│ └── tananaev
│ └── adblib
│ ├── AdbAuthenticationFailedException.java
│ ├── AdbBase64.java
│ ├── AdbConnection.java
│ ├── AdbCrypto.java
│ ├── AdbProtocol.java
│ └── AdbStream.java
└── test
└── java
└── com
└── tananaev
└── adblib
├── AdbConnectionTest.java
└── AdbStreamTest.java
/.github/workflows/gradle.yml:
--------------------------------------------------------------------------------
1 | name: Java CI with Gradle
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: actions/setup-java@v1
17 | with:
18 | java-version: 1.8
19 | - run: chmod +x gradlew
20 | - run: ./gradlew build
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .gradle/
3 | build/
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, Cameron Gutman
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | Redistributions in binary form must reproduce the above copyright notice, this
11 | list of conditions and the following disclaimer in the documentation and/or
12 | other materials provided with the distribution.
13 |
14 | Neither the name of the {organization} nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ADB Library - adblib
2 |
3 | ## Overview
4 |
5 | A Java library implementation of [the ADB (Android Debug Bridge) network protocol](https://android.googlesource.com/platform/system/core/+/master/adb/protocol.txt).
6 |
7 | This project is a fork of the [original library](https://github.com/cgutman/AdbLib) developed by Cameron Gutman.
8 |
9 | ## Usage
10 |
11 | Include dependency via Gradle:
12 | ```groovy
13 | compile 'com.tananaev:adblib:1.3'
14 | ```
15 | or Maven:
16 | ```xml
17 |
18 | com.tananaev
19 | adblib
20 | 1.3
21 |
22 | ```
23 |
24 | To be able to connect to the ADB daemon on Android phone, you need to enable it to listen to TCP connections. To do that, connect your phone via USB cable and run following adb command:
25 | ```
26 | adb tcpip 5555
27 | ```
28 |
29 | Disconnect USB cable before trying to connect using the library. Some phones have problems handling TCP connection when they are connected via USB as well.
30 |
31 | More info about Android remote debugging can be found on the official [Android developer website](https://developer.android.com/studio/command-line/adb.html#wireless).
32 |
33 | Sample library usage example:
34 | ```java
35 | Socket socket = new Socket("192.168.1.42", 5555); // put phone IP address here
36 |
37 | AdbCrypto crypto = AdbCrypto.generateAdbKeyPair(new AdbBase64() {
38 | @Override
39 | public String encodeToString(byte[] data) {
40 | return DatatypeConverter.printBase64Binary(data);
41 | }
42 | });
43 |
44 | AdbConnection connection = AdbConnection.create(socket, crypto);
45 | connection.connect();
46 |
47 | AdbStream stream = connection.open("shell:logcat");
48 |
49 | ...
50 | ```
51 |
52 | ## License
53 |
54 | Copyright (c) 2013, Cameron Gutman
55 | All rights reserved.
56 |
57 | Redistribution and use in source and binary forms, with or without modification,
58 | are permitted provided that the following conditions are met:
59 |
60 | Redistributions of source code must retain the above copyright notice, this
61 | list of conditions and the following disclaimer.
62 |
63 | Redistributions in binary form must reproduce the above copyright notice, this
64 | list of conditions and the following disclaimer in the documentation and/or
65 | other materials provided with the distribution.
66 |
67 | Neither the name of the {organization} nor the names of its
68 | contributors may be used to endorse or promote products derived from
69 | this software without specific prior written permission.
70 |
71 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
72 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
73 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
74 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
75 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
76 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
77 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
78 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
79 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
80 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
81 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java'
2 | apply plugin: 'maven'
3 | apply plugin: 'signing'
4 |
5 | group = 'com.tananaev'
6 | version = '1.3'
7 |
8 | sourceCompatibility = 1.7
9 |
10 | repositories {
11 | jcenter()
12 | }
13 |
14 | dependencies {
15 | testCompile 'junit:junit:4.12'
16 | }
17 |
18 | if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) {
19 |
20 | task javadocJar(type: Jar) {
21 | classifier = 'javadoc'
22 | from javadoc
23 | }
24 |
25 | task sourcesJar(type: Jar) {
26 | classifier = 'sources'
27 | from sourceSets.main.allSource
28 | }
29 |
30 | artifacts {
31 | archives javadocJar, sourcesJar
32 | }
33 |
34 | signing {
35 | sign configurations.archives
36 | }
37 |
38 | uploadArchives {
39 | repositories {
40 | mavenDeployer {
41 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
42 |
43 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
44 | authentication(userName: ossrhUsername, password: ossrhPassword)
45 | }
46 |
47 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
48 | authentication(userName: ossrhUsername, password: ossrhPassword)
49 | }
50 |
51 | pom.project {
52 | name 'adblib'
53 | packaging 'jar'
54 | description 'A Java library implementation of the ADB (Android Debug Bridge) network protocol.'
55 | url 'https://github.com/tananaev/adblib'
56 |
57 | scm {
58 | connection 'scm:git:https://github.com/tananaev/adblib'
59 | developerConnection 'scm:git:git@github.com:tananaev/adblib.git'
60 | url 'https://github.com/tananaev/adblib'
61 | }
62 |
63 | licenses {
64 | license {
65 | name 'BSD 3-Clause License'
66 | url 'http://opensource.org/licenses/BSD-3-Clause'
67 | }
68 | }
69 |
70 | developers {
71 | developer {
72 | id 'tananaev'
73 | name 'Anton Tananaev'
74 | email 'anton.tananaev@gmail.com'
75 | }
76 | }
77 | }
78 | }
79 | }
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tananaev/adblib/4b3d25641933b92b729ee67d17791992158d20be/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn ( ) {
37 | echo "$*"
38 | }
39 |
40 | die ( ) {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'adblib'
2 |
--------------------------------------------------------------------------------
/src/main/java/com/tananaev/adblib/AdbAuthenticationFailedException.java:
--------------------------------------------------------------------------------
1 | package com.tananaev.adblib;
2 |
3 | /**
4 | * Thrown when the peer rejects our initial authentication attempt,
5 | * which typically means that the peer has not previously saved our
6 | * public key.
7 | *
8 | * This is an unchecked exception for backwards-compatibility.
9 | */
10 | public class AdbAuthenticationFailedException extends RuntimeException {
11 |
12 | public AdbAuthenticationFailedException() {
13 | super("Initial authentication attempt rejected by peer");
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/com/tananaev/adblib/AdbBase64.java:
--------------------------------------------------------------------------------
1 | package com.tananaev.adblib;
2 |
3 | /**
4 | * This interface specifies the required functions for AdbCrypto to
5 | * perform Base64 encoding of its public key.
6 | *
7 | * @author Cameron Gutman
8 | */
9 | public interface AdbBase64 {
10 |
11 | /**
12 | * This function must encoded the specified data as a base 64 string, without
13 | * appending any extra newlines or other characters.
14 | *
15 | * @param data Data to encode
16 | * @return String containing base 64 encoded data
17 | */
18 | String encodeToString(byte[] data);
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/com/tananaev/adblib/AdbConnection.java:
--------------------------------------------------------------------------------
1 | package com.tananaev.adblib;
2 |
3 | import java.io.Closeable;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.io.OutputStream;
7 | import java.io.UnsupportedEncodingException;
8 | import java.net.ConnectException;
9 | import java.net.Socket;
10 | import java.util.concurrent.ConcurrentHashMap;
11 | import java.util.concurrent.TimeUnit;
12 |
13 | /**
14 | * This class represents an ADB connection.
15 | *
16 | * @author Cameron Gutman
17 | */
18 | public class AdbConnection implements Closeable {
19 |
20 | /**
21 | * The underlying socket that this class uses to
22 | * communicate with the target device.
23 | */
24 | private Socket socket;
25 |
26 | /**
27 | * The last allocated local stream ID. The ID
28 | * chosen for the next stream will be this value + 1.
29 | */
30 | private int lastLocalId;
31 |
32 | /**
33 | * The input stream that this class uses to read from
34 | * the socket.
35 | */
36 | private volatile InputStream inputStream;
37 |
38 | /**
39 | * The output stream that this class uses to read from
40 | * the socket.
41 | */
42 | volatile OutputStream outputStream;
43 |
44 | /**
45 | * The backend thread that handles responding to ADB packets.
46 | */
47 | private volatile Thread connectionThread;
48 |
49 | /**
50 | * Specifies whether a connect has been attempted
51 | */
52 | private volatile boolean connectAttempted;
53 |
54 | /**
55 | * Whether the connection thread should give up if the first authentication attempt fails
56 | */
57 | private volatile boolean abortOnUnauthorised;
58 |
59 | /**
60 | * Whether the the first authentication attempt failed and {@link #abortOnUnauthorised} was {@code true}
61 | */
62 | private volatile boolean authorisationFailed;
63 |
64 | /**
65 | * Specifies whether a CNXN packet has been received from the peer.
66 | */
67 | private volatile boolean connected;
68 |
69 | /**
70 | * Specifies the maximum amount data that can be sent to the remote peer.
71 | * This is only valid after connect() returns successfully.
72 | */
73 | private volatile int maxData;
74 |
75 | /**
76 | * An initialized ADB crypto object that contains a key pair.
77 | */
78 | private volatile com.tananaev.adblib.AdbCrypto crypto;
79 |
80 | /**
81 | * Specifies whether this connection has already sent a signed token.
82 | */
83 | private boolean sentSignature;
84 |
85 | /**
86 | * A hash map of our open streams indexed by local ID.
87 | **/
88 | private volatile ConcurrentHashMap openStreams;
89 |
90 | /**
91 | * Internal constructor to initialize some internal state
92 | */
93 | private AdbConnection() {
94 | openStreams = new ConcurrentHashMap();
95 | lastLocalId = 0;
96 | connectionThread = createConnectionThread();
97 | }
98 |
99 | /**
100 | * Creates a AdbConnection object associated with the socket and
101 | * crypto object specified.
102 | *
103 | * @param socket The socket that the connection will use for communcation.
104 | * @param crypto The crypto object that stores the key pair for authentication.
105 | * @return A new AdbConnection object.
106 | * @throws IOException If there is a socket error
107 | */
108 | public static AdbConnection create(Socket socket, AdbCrypto crypto) throws IOException {
109 | AdbConnection newConn = new AdbConnection();
110 |
111 | newConn.crypto = crypto;
112 |
113 | newConn.socket = socket;
114 | newConn.inputStream = socket.getInputStream();
115 | newConn.outputStream = socket.getOutputStream();
116 |
117 | /* Disable Nagle because we're sending tiny packets */
118 | socket.setTcpNoDelay(true);
119 |
120 | return newConn;
121 | }
122 |
123 | /**
124 | * Creates a new connection thread.
125 | *
126 | * @return A new connection thread.
127 | */
128 | private Thread createConnectionThread() {
129 | @SuppressWarnings("resource")
130 | final AdbConnection conn = this;
131 | return new Thread(new Runnable() {
132 | @Override
133 | public void run() {
134 | while (!connectionThread.isInterrupted()) {
135 | try {
136 | /* Read and parse a message off the socket's input stream */
137 | AdbProtocol.AdbMessage msg = AdbProtocol.AdbMessage.parseAdbMessage(inputStream);
138 |
139 | /* Verify magic and checksum */
140 | if (!AdbProtocol.validateMessage(msg))
141 | continue;
142 |
143 | switch (msg.command) {
144 | /* Stream-oriented commands */
145 | case AdbProtocol.CMD_OKAY:
146 | case AdbProtocol.CMD_WRTE:
147 | case AdbProtocol.CMD_CLSE:
148 | /* We must ignore all packets when not connected */
149 | if (!conn.connected)
150 | continue;
151 |
152 | /* Get the stream object corresponding to the packet */
153 | AdbStream waitingStream = openStreams.get(msg.arg1);
154 | if (waitingStream == null)
155 | continue;
156 |
157 | synchronized (waitingStream) {
158 | if (msg.command == AdbProtocol.CMD_OKAY) {
159 | /* We're ready for writes */
160 | waitingStream.updateRemoteId(msg.arg0);
161 | waitingStream.readyForWrite();
162 |
163 | /* Unwait an open/write */
164 | waitingStream.notify();
165 | } else if (msg.command == AdbProtocol.CMD_WRTE) {
166 | /* Got some data from our partner */
167 | waitingStream.addPayload(msg.payload);
168 |
169 | /* Tell it we're ready for more */
170 | waitingStream.sendReady();
171 | } else if (msg.command == AdbProtocol.CMD_CLSE) {
172 | /* He doesn't like us anymore :-( */
173 | conn.openStreams.remove(msg.arg1);
174 |
175 | /* Notify readers and writers */
176 | waitingStream.notifyClose(true);
177 | }
178 | }
179 |
180 | break;
181 |
182 | case AdbProtocol.CMD_AUTH:
183 |
184 | byte[] packet;
185 |
186 | if (msg.arg0 == AdbProtocol.AUTH_TYPE_TOKEN) {
187 | /* This is an authentication challenge */
188 | if (conn.sentSignature) {
189 | if (abortOnUnauthorised) {
190 | authorisationFailed = true;
191 | /* Throwing an exception to break out of the loop */
192 | throw new RuntimeException();
193 | }
194 |
195 | /* We've already tried our signature, so send our public key */
196 | packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_RSA_PUBLIC,
197 | conn.crypto.getAdbPublicKeyPayload());
198 | } else {
199 | /* We'll sign the token */
200 | packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_SIGNATURE,
201 | conn.crypto.signAdbTokenPayload(msg.payload));
202 | conn.sentSignature = true;
203 | }
204 |
205 | /* Write the AUTH reply */
206 | synchronized (conn.outputStream) {
207 | conn.outputStream.write(packet);
208 | conn.outputStream.flush();
209 | }
210 | }
211 | break;
212 |
213 | case AdbProtocol.CMD_CNXN:
214 | synchronized (conn) {
215 | /* We need to store the max data size */
216 | conn.maxData = msg.arg1;
217 |
218 | /* Mark us as connected and unwait anyone waiting on the connection */
219 | conn.connected = true;
220 | conn.notifyAll();
221 | }
222 | break;
223 |
224 | default:
225 | /* Unrecognized packet, just drop it */
226 | break;
227 | }
228 | } catch (Exception e) {
229 | /* The cleanup is taken care of by a combination of this thread
230 | * and close() */
231 | break;
232 | }
233 | }
234 |
235 | /* This thread takes care of cleaning up pending streams */
236 | synchronized (conn) {
237 | cleanupStreams();
238 | conn.notifyAll();
239 | conn.connectAttempted = false;
240 | }
241 | }
242 | });
243 | }
244 |
245 | /**
246 | * Gets the max data size that the remote client supports.
247 | * A connection must have been attempted before calling this routine.
248 | * This routine will block if a connection is in progress.
249 | *
250 | * @return The maximum data size indicated in the connect packet.
251 | * @throws InterruptedException If a connection cannot be waited on.
252 | * @throws IOException if the connection fails
253 | */
254 | public int getMaxData() throws InterruptedException, IOException {
255 | if (!connectAttempted)
256 | throw new IllegalStateException("connect() must be called first");
257 |
258 | waitForConnection(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
259 |
260 | return maxData;
261 | }
262 |
263 | /**
264 | * Same as {@code connect(Long.MAX_VALUE, TimeUnit.MILLISECONDS, false)}
265 | *
266 | * @throws IOException If the socket fails while connecting
267 | * @throws InterruptedException If we are unable to wait for the connection to finish
268 | */
269 | public void connect() throws IOException, InterruptedException {
270 | connect(Long.MAX_VALUE, TimeUnit.MILLISECONDS, false);
271 | }
272 |
273 | /**
274 | * Connects to the remote device. This routine will block until the connection
275 | * completes or the timeout elapses.
276 | *
277 | * @param timeout the time to wait for the lock
278 | * @param unit the time unit of the timeout argument
279 | * @param throwOnUnauthorised Whether to throw an {@link AdbAuthenticationFailedException}
280 | * if the peer rejects out first authentication attempt
281 | * @return {@code true} if the connection was established, or {@code false} if the connection timed out
282 | * @throws IOException If the socket fails while connecting
283 | * @throws InterruptedException If we are unable to wait for the connection to finish
284 | * @throws AdbAuthenticationFailedException If {@code throwOnUnauthorised} is {@code true}
285 | * and the peer rejects the first authentication attempt, which indicates that the peer has
286 | * not saved our public key from a previous connection
287 | */
288 | public boolean connect(long timeout, TimeUnit unit, boolean throwOnUnauthorised) throws IOException, InterruptedException, AdbAuthenticationFailedException {
289 | if (connected)
290 | throw new IllegalStateException("Already connected");
291 |
292 | /* Write the CONNECT packet */
293 | synchronized (outputStream) {
294 | outputStream.write(AdbProtocol.generateConnect());
295 | outputStream.flush();
296 | }
297 |
298 | /* Start the connection thread to respond to the peer */
299 | connectAttempted = true;
300 | abortOnUnauthorised = throwOnUnauthorised;
301 | authorisationFailed = false;
302 | connectionThread.start();
303 |
304 | return waitForConnection(timeout, unit);
305 | }
306 |
307 | /**
308 | * Opens an AdbStream object corresponding to the specified destination.
309 | * This routine will block until the connection completes.
310 | *
311 | * @param destination The destination to open on the target
312 | * @return AdbStream object corresponding to the specified destination
313 | * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8
314 | * @throws IOException If the stream fails while sending the packet
315 | * @throws InterruptedException If we are unable to wait for the connection to finish
316 | */
317 | public AdbStream open(String destination) throws UnsupportedEncodingException, IOException, InterruptedException {
318 | int localId = ++lastLocalId;
319 |
320 | if (!connectAttempted)
321 | throw new IllegalStateException("connect() must be called first");
322 |
323 | waitForConnection(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
324 |
325 | /* Add this stream to this list of half-open streams */
326 | AdbStream stream = new AdbStream(this, localId);
327 | openStreams.put(localId, stream);
328 |
329 | /* Send the open */
330 | synchronized (outputStream) {
331 | outputStream.write(AdbProtocol.generateOpen(localId, destination));
332 | outputStream.flush();
333 | }
334 |
335 | /* Wait for the connection thread to receive the OKAY */
336 | synchronized (stream) {
337 | stream.wait();
338 | }
339 |
340 | /* Check if the open was rejected */
341 | if (stream.isClosed())
342 | throw new ConnectException("Stream open actively rejected by remote peer");
343 |
344 | /* We're fully setup now */
345 | return stream;
346 | }
347 |
348 | private boolean waitForConnection(long timeout, TimeUnit unit) throws InterruptedException, IOException {
349 | synchronized (this) {
350 | /* Block if a connection is pending, but not yet complete */
351 | long timeoutEndMillis = System.currentTimeMillis() + unit.toMillis(timeout);
352 | while (!connected && connectAttempted && timeoutEndMillis - System.currentTimeMillis() > 0) {
353 | wait(timeoutEndMillis - System.currentTimeMillis());
354 | }
355 |
356 | if (!connected) {
357 | if (connectAttempted)
358 | return false;
359 | else if (authorisationFailed)
360 | throw new AdbAuthenticationFailedException();
361 | else
362 | throw new IOException("Connection failed");
363 | }
364 | }
365 |
366 | return true;
367 | }
368 |
369 | /**
370 | * This function terminates all I/O on streams associated with this ADB connection
371 | */
372 | private void cleanupStreams() {
373 | /* Close all streams on this connection */
374 | for (AdbStream s : openStreams.values()) {
375 | /* We handle exceptions for each close() call to avoid
376 | * terminating cleanup for one failed close(). */
377 | try {
378 | s.close();
379 | } catch (IOException e) {
380 | }
381 | }
382 |
383 | /* No open streams anymore */
384 | openStreams.clear();
385 | }
386 |
387 | /**
388 | * This routine closes the Adb connection and underlying socket
389 | *
390 | * @throws IOException if the socket fails to close
391 | */
392 | @Override
393 | public void close() throws IOException {
394 | /* If the connection thread hasn't spawned yet, there's nothing to do */
395 | if (connectionThread == null)
396 | return;
397 |
398 | /* Closing the socket will kick the connection thread */
399 | socket.close();
400 |
401 | /* Wait for the connection thread to die */
402 | connectionThread.interrupt();
403 | try {
404 | connectionThread.join();
405 | } catch (InterruptedException e) {
406 | }
407 | }
408 |
409 | }
410 |
--------------------------------------------------------------------------------
/src/main/java/com/tananaev/adblib/AdbCrypto.java:
--------------------------------------------------------------------------------
1 | package com.tananaev.adblib;
2 |
3 | import java.io.File;
4 | import java.io.FileInputStream;
5 | import java.io.FileOutputStream;
6 | import java.io.IOException;
7 | import java.math.BigInteger;
8 | import java.nio.ByteBuffer;
9 | import java.nio.ByteOrder;
10 | import java.security.GeneralSecurityException;
11 | import java.security.KeyFactory;
12 | import java.security.KeyPair;
13 | import java.security.KeyPairGenerator;
14 | import java.security.NoSuchAlgorithmException;
15 | import java.security.interfaces.RSAPublicKey;
16 | import java.security.spec.EncodedKeySpec;
17 | import java.security.spec.InvalidKeySpecException;
18 | import java.security.spec.PKCS8EncodedKeySpec;
19 | import java.security.spec.X509EncodedKeySpec;
20 | import javax.crypto.Cipher;
21 |
22 | /**
23 | * This class encapsulates the ADB cryptography functions and provides
24 | * an interface for the storage and retrieval of keys.
25 | *
26 | * @author Cameron Gutman
27 | */
28 | public class AdbCrypto {
29 |
30 | /**
31 | * An RSA keypair encapsulated by the AdbCrypto object
32 | */
33 | private KeyPair keyPair;
34 |
35 | /**
36 | * The base 64 conversion interface to use
37 | */
38 | private AdbBase64 base64;
39 |
40 | /**
41 | * The ADB RSA key length in bits
42 | */
43 | public static final int KEY_LENGTH_BITS = 2048;
44 |
45 | /**
46 | * The ADB RSA key length in bytes
47 | */
48 | public static final int KEY_LENGTH_BYTES = KEY_LENGTH_BITS / 8;
49 |
50 | /**
51 | * The ADB RSA key length in words
52 | */
53 | public static final int KEY_LENGTH_WORDS = KEY_LENGTH_BYTES / 4;
54 |
55 | /**
56 | * The RSA signature padding as an int array
57 | */
58 | public static final int[] SIGNATURE_PADDING_AS_INT = new int[] {
59 | 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
60 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
61 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
62 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
63 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
64 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
65 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
66 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
67 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
68 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
69 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
70 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
71 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
72 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
73 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
74 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
75 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00,
76 | 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00,
77 | 0x04, 0x14
78 | };
79 |
80 | /**
81 | * The RSA signature padding as a byte array
82 | */
83 | public static byte[] SIGNATURE_PADDING;
84 |
85 | static {
86 | SIGNATURE_PADDING = new byte[SIGNATURE_PADDING_AS_INT.length];
87 |
88 | for (int i = 0; i < SIGNATURE_PADDING.length; i++)
89 | SIGNATURE_PADDING[i] = (byte) SIGNATURE_PADDING_AS_INT[i];
90 | }
91 |
92 | /**
93 | * Converts a standard RSAPublicKey object to the special ADB format
94 | *
95 | * @param pubkey RSAPublicKey object to convert
96 | * @return Byte array containing the converted RSAPublicKey object
97 | */
98 | private static byte[] convertRsaPublicKeyToAdbFormat(RSAPublicKey pubkey) {
99 | /*
100 | * ADB literally just saves the RSAPublicKey struct to a file.
101 | *
102 | * typedef struct RSAPublicKey {
103 | * int len; // Length of n[] in number of uint32_t
104 | * uint32_t n0inv; // -1 / n[0] mod 2^32
105 | * uint32_t n[RSANUMWORDS]; // modulus as little endian array
106 | * uint32_t rr[RSANUMWORDS]; // R^2 as little endian array
107 | * int exponent; // 3 or 65537
108 | * } RSAPublicKey;
109 | */
110 |
111 | /* ------ This part is a Java-ified version of RSA_to_RSAPublicKey from adb_host_auth.c ------ */
112 | BigInteger r32, r, rr, rem, n, n0inv;
113 |
114 | r32 = BigInteger.ZERO.setBit(32);
115 | n = pubkey.getModulus();
116 | r = BigInteger.ZERO.setBit(KEY_LENGTH_WORDS * 32);
117 | rr = r.modPow(BigInteger.valueOf(2), n);
118 | rem = n.remainder(r32);
119 | n0inv = rem.modInverse(r32);
120 |
121 | int myN[] = new int[KEY_LENGTH_WORDS];
122 | int myRr[] = new int[KEY_LENGTH_WORDS];
123 | BigInteger res[];
124 | for (int i = 0; i < KEY_LENGTH_WORDS; i++) {
125 | res = rr.divideAndRemainder(r32);
126 | rr = res[0];
127 | rem = res[1];
128 | myRr[i] = rem.intValue();
129 |
130 | res = n.divideAndRemainder(r32);
131 | n = res[0];
132 | rem = res[1];
133 | myN[i] = rem.intValue();
134 | }
135 |
136 | /* ------------------------------------------------------------------------------------------- */
137 |
138 | ByteBuffer bbuf = ByteBuffer.allocate(524).order(ByteOrder.LITTLE_ENDIAN);
139 |
140 |
141 | bbuf.putInt(KEY_LENGTH_WORDS);
142 | bbuf.putInt(n0inv.negate().intValue());
143 | for (int i : myN)
144 | bbuf.putInt(i);
145 | for (int i : myRr)
146 | bbuf.putInt(i);
147 |
148 | bbuf.putInt(pubkey.getPublicExponent().intValue());
149 | return bbuf.array();
150 | }
151 |
152 | /**
153 | * Creates a new AdbCrypto object from a key pair loaded from files.
154 | *
155 | * @param base64 Implementation of base 64 conversion interface required by ADB
156 | * @param privateKey File containing the RSA private key
157 | * @param publicKey File containing the RSA public key
158 | * @return New AdbCrypto object
159 | * @throws IOException If the files cannot be read
160 | * @throws NoSuchAlgorithmException If an RSA key factory cannot be found
161 | * @throws InvalidKeySpecException If a PKCS8 or X509 key spec cannot be found
162 | */
163 | public static AdbCrypto loadAdbKeyPair(AdbBase64 base64, File privateKey, File publicKey) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
164 | AdbCrypto crypto = new AdbCrypto();
165 |
166 | int privKeyLength = (int) privateKey.length();
167 | int pubKeyLength = (int) publicKey.length();
168 | byte[] privKeyBytes = new byte[privKeyLength];
169 | byte[] pubKeyBytes = new byte[pubKeyLength];
170 |
171 | FileInputStream privIn = new FileInputStream(privateKey);
172 | FileInputStream pubIn = new FileInputStream(publicKey);
173 |
174 | privIn.read(privKeyBytes);
175 | pubIn.read(pubKeyBytes);
176 |
177 | privIn.close();
178 | pubIn.close();
179 |
180 | KeyFactory keyFactory = KeyFactory.getInstance("RSA");
181 | EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);
182 | EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(pubKeyBytes);
183 |
184 | crypto.keyPair = new KeyPair(keyFactory.generatePublic(publicKeySpec),
185 | keyFactory.generatePrivate(privateKeySpec));
186 | crypto.base64 = base64;
187 |
188 | return crypto;
189 | }
190 |
191 | /**
192 | * Creates a new AdbCrypto object from a key pair loaded from files.
193 | *
194 | * @param base64 Implementation of base 64 conversion interface required by ADB
195 | * @param keyPair RSA key pair
196 | * @return New AdbCrypto object
197 | */
198 | public static AdbCrypto loadAdbKeyPair(AdbBase64 base64, KeyPair keyPair) {
199 | AdbCrypto crypto = new AdbCrypto();
200 |
201 | crypto.keyPair = keyPair;
202 | crypto.base64 = base64;
203 |
204 | return crypto;
205 | }
206 |
207 | /**
208 | * Creates a new AdbCrypto object by generating a new key pair.
209 | *
210 | * @param base64 Implementation of base 64 conversion interface required by ADB
211 | * @return A new AdbCrypto object
212 | * @throws NoSuchAlgorithmException If an RSA key factory cannot be found
213 | */
214 | public static AdbCrypto generateAdbKeyPair(AdbBase64 base64) throws NoSuchAlgorithmException {
215 | AdbCrypto crypto = new AdbCrypto();
216 |
217 | KeyPairGenerator rsaKeyPg = KeyPairGenerator.getInstance("RSA");
218 | rsaKeyPg.initialize(KEY_LENGTH_BITS);
219 |
220 | crypto.keyPair = rsaKeyPg.genKeyPair();
221 | crypto.base64 = base64;
222 |
223 | return crypto;
224 | }
225 |
226 | /**
227 | * Signs the ADB SHA1 payload with the private key of this object.
228 | *
229 | * @param payload SHA1 payload to sign
230 | * @return Signed SHA1 payload
231 | * @throws GeneralSecurityException If signing fails
232 | */
233 | public byte[] signAdbTokenPayload(byte[] payload) throws GeneralSecurityException {
234 | Cipher c = Cipher.getInstance("RSA/ECB/NoPadding");
235 |
236 | c.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate());
237 |
238 | c.update(SIGNATURE_PADDING);
239 |
240 | return c.doFinal(payload);
241 | }
242 |
243 | /**
244 | * Gets the RSA public key in ADB format.
245 | *
246 | * @return Byte array containing the RSA public key in ADB format.
247 | * @throws IOException If the key cannot be retrived
248 | */
249 | public byte[] getAdbPublicKeyPayload() throws IOException {
250 | byte[] convertedKey = convertRsaPublicKeyToAdbFormat((RSAPublicKey) keyPair.getPublic());
251 | StringBuilder keyString = new StringBuilder(720);
252 |
253 | /* The key is base64 encoded with a user@host suffix and terminated with a NUL */
254 | keyString.append(base64.encodeToString(convertedKey));
255 | keyString.append(" unknown@unknown");
256 | keyString.append('\0');
257 |
258 | return keyString.toString().getBytes("UTF-8");
259 | }
260 |
261 | /**
262 | * Saves the AdbCrypto's key pair to the specified files.
263 | *
264 | * @param privateKey The file to store the encoded private key
265 | * @param publicKey The file to store the encoded public key
266 | * @throws IOException If the files cannot be written
267 | */
268 | public void saveAdbKeyPair(File privateKey, File publicKey) throws IOException {
269 | FileOutputStream privOut = new FileOutputStream(privateKey);
270 | FileOutputStream pubOut = new FileOutputStream(publicKey);
271 |
272 | privOut.write(keyPair.getPrivate().getEncoded());
273 | pubOut.write(keyPair.getPublic().getEncoded());
274 |
275 | privOut.close();
276 | pubOut.close();
277 | }
278 |
279 | }
280 |
--------------------------------------------------------------------------------
/src/main/java/com/tananaev/adblib/AdbProtocol.java:
--------------------------------------------------------------------------------
1 | package com.tananaev.adblib;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 | import java.io.UnsupportedEncodingException;
6 | import java.nio.ByteBuffer;
7 | import java.nio.ByteOrder;
8 |
9 | /**
10 | * This class provides useful functions and fields for ADB protocol details.
11 | *
12 | * @author Cameron Gutman
13 | */
14 | public class AdbProtocol {
15 |
16 | /**
17 | * The length of the ADB message header
18 | */
19 | public static final int ADB_HEADER_LENGTH = 24;
20 |
21 | public static final int CMD_SYNC = 0x434e5953;
22 |
23 | /**
24 | * CNXN is the connect message. No messages (except AUTH)
25 | * are valid before this message is received.
26 | */
27 | public static final int CMD_CNXN = 0x4e584e43;
28 |
29 | /**
30 | * The current version of the ADB protocol
31 | */
32 | public static final int CONNECT_VERSION = 0x01000000;
33 |
34 | /**
35 | * The maximum data payload supported by the ADB implementation
36 | */
37 | public static final int CONNECT_MAXDATA = 4096;
38 |
39 | /**
40 | * The payload sent with the connect message
41 | */
42 | public static byte[] CONNECT_PAYLOAD;
43 |
44 | static {
45 | try {
46 | CONNECT_PAYLOAD = "host::\0".getBytes("UTF-8");
47 | } catch (UnsupportedEncodingException e) {
48 | }
49 | }
50 |
51 | /**
52 | * AUTH is the authentication message. It is part of the
53 | * RSA public key authentication added in Android 4.2.2.
54 | */
55 | public static final int CMD_AUTH = 0x48545541;
56 |
57 | /**
58 | * This authentication type represents a SHA1 hash to sign
59 | */
60 | public static final int AUTH_TYPE_TOKEN = 1;
61 |
62 | /**
63 | * This authentication type represents the signed SHA1 hash
64 | */
65 | public static final int AUTH_TYPE_SIGNATURE = 2;
66 |
67 | /**
68 | * This authentication type represents a RSA public key
69 | */
70 | public static final int AUTH_TYPE_RSA_PUBLIC = 3;
71 |
72 | /**
73 | * OPEN is the open stream message. It is sent to open
74 | * a new stream on the target device.
75 | */
76 | public static final int CMD_OPEN = 0x4e45504f;
77 |
78 | /**
79 | * OKAY is a success message. It is sent when a write is
80 | * processed successfully.
81 | */
82 | public static final int CMD_OKAY = 0x59414b4f;
83 |
84 | /**
85 | * CLSE is the close stream message. It it sent to close an
86 | * existing stream on the target device.
87 | */
88 | public static final int CMD_CLSE = 0x45534c43;
89 |
90 | /**
91 | * WRTE is the write stream message. It is sent with a payload
92 | * that is the data to write to the stream.
93 | */
94 | public static final int CMD_WRTE = 0x45545257;
95 |
96 | /**
97 | * This function performs a checksum on the ADB payload data.
98 | *
99 | * @param payload Payload to checksum
100 | * @return The checksum of the payload
101 | */
102 | private static int getPayloadChecksum(byte[] payload) {
103 | int checksum = 0;
104 |
105 | for (byte b : payload) {
106 | /* We have to manually "unsign" these bytes because Java sucks */
107 | if (b >= 0)
108 | checksum += b;
109 | else
110 | checksum += b + 256;
111 | }
112 |
113 | return checksum;
114 | }
115 |
116 | /**
117 | * This function validate the ADB message by checking
118 | * its command, magic, and payload checksum.
119 | *
120 | * @param msg ADB message to validate
121 | * @return True if the message was valid, false otherwise
122 | */
123 | public static boolean validateMessage(AdbMessage msg) {
124 | /* Magic is cmd ^ 0xFFFFFFFF */
125 | if (msg.command != (msg.magic ^ 0xFFFFFFFF))
126 | return false;
127 |
128 | if (msg.payloadLength != 0) {
129 | if (getPayloadChecksum(msg.payload) != msg.checksum)
130 | return false;
131 | }
132 |
133 | return true;
134 | }
135 |
136 | /**
137 | * This function generates an ADB message given the fields.
138 | *
139 | * @param cmd Command identifier
140 | * @param arg0 First argument
141 | * @param arg1 Second argument
142 | * @param payload Data payload
143 | * @return Byte array containing the message
144 | */
145 | public static byte[] generateMessage(int cmd, int arg0, int arg1, byte[] payload) {
146 | /* struct message {
147 | * unsigned command; // command identifier constant
148 | * unsigned arg0; // first argument
149 | * unsigned arg1; // second argument
150 | * unsigned data_length; // length of payload (0 is allowed)
151 | * unsigned data_check; // checksum of data payload
152 | * unsigned magic; // command ^ 0xffffffff
153 | * };
154 | */
155 |
156 | ByteBuffer message;
157 |
158 | if (payload != null) {
159 | message = ByteBuffer.allocate(ADB_HEADER_LENGTH + payload.length).order(ByteOrder.LITTLE_ENDIAN);
160 | } else {
161 | message = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
162 | }
163 |
164 | message.putInt(cmd);
165 | message.putInt(arg0);
166 | message.putInt(arg1);
167 |
168 | if (payload != null) {
169 | message.putInt(payload.length);
170 | message.putInt(getPayloadChecksum(payload));
171 | } else {
172 | message.putInt(0);
173 | message.putInt(0);
174 | }
175 |
176 | message.putInt(cmd ^ 0xFFFFFFFF);
177 |
178 | if (payload != null) {
179 | message.put(payload);
180 | }
181 |
182 | return message.array();
183 | }
184 |
185 | /**
186 | * Generates a connect message with default parameters.
187 | *
188 | * @return Byte array containing the message
189 | */
190 | public static byte[] generateConnect() {
191 | return generateMessage(CMD_CNXN, CONNECT_VERSION, CONNECT_MAXDATA, CONNECT_PAYLOAD);
192 | }
193 |
194 | /**
195 | * Generates an auth message with the specified type and payload.
196 | *
197 | * @param type Authentication type (see AUTH_TYPE_* constants)
198 | * @param data The payload for the message
199 | * @return Byte array containing the message
200 | */
201 | public static byte[] generateAuth(int type, byte[] data) {
202 | return generateMessage(CMD_AUTH, type, 0, data);
203 | }
204 |
205 | /**
206 | * Generates an open stream message with the specified local ID and destination.
207 | *
208 | * @param localId A unique local ID identifying the stream
209 | * @param dest The destination of the stream on the target
210 | * @return Byte array containing the message
211 | * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8
212 | */
213 | public static byte[] generateOpen(int localId, String dest) throws UnsupportedEncodingException {
214 | ByteBuffer bbuf = ByteBuffer.allocate(dest.length() + 1);
215 | bbuf.put(dest.getBytes("UTF-8"));
216 | bbuf.put((byte) 0);
217 | return generateMessage(CMD_OPEN, localId, 0, bbuf.array());
218 | }
219 |
220 | /**
221 | * Generates a write stream message with the specified IDs and payload.
222 | *
223 | * @param localId The unique local ID of the stream
224 | * @param remoteId The unique remote ID of the stream
225 | * @param data The data to provide as the write payload
226 | * @return Byte array containing the message
227 | */
228 | public static byte[] generateWrite(int localId, int remoteId, byte[] data) {
229 | return generateMessage(CMD_WRTE, localId, remoteId, data);
230 | }
231 |
232 | /**
233 | * Generates a close stream message with the specified IDs.
234 | *
235 | * @param localId The unique local ID of the stream
236 | * @param remoteId The unique remote ID of the stream
237 | * @return Byte array containing the message
238 | */
239 | public static byte[] generateClose(int localId, int remoteId) {
240 | return generateMessage(CMD_CLSE, localId, remoteId, null);
241 | }
242 |
243 | /**
244 | * Generates an okay message with the specified IDs.
245 | *
246 | * @param localId The unique local ID of the stream
247 | * @param remoteId The unique remote ID of the stream
248 | * @return Byte array containing the message
249 | */
250 | public static byte[] generateReady(int localId, int remoteId) {
251 | return generateMessage(CMD_OKAY, localId, remoteId, null);
252 | }
253 |
254 | /**
255 | * This class provides an abstraction for the ADB message format.
256 | *
257 | * @author Cameron Gutman
258 | */
259 | final static class AdbMessage {
260 | /**
261 | * The command field of the message
262 | */
263 | public int command;
264 | /**
265 | * The arg0 field of the message
266 | */
267 | public int arg0;
268 | /**
269 | * The arg1 field of the message
270 | */
271 | public int arg1;
272 | /**
273 | * The payload length field of the message
274 | */
275 | public int payloadLength;
276 | /**
277 | * The checksum field of the message
278 | */
279 | public int checksum;
280 | /**
281 | * The magic field of the message
282 | */
283 | public int magic;
284 | /**
285 | * The payload of the message
286 | */
287 | public byte[] payload;
288 |
289 | /**
290 | * Read and parse an ADB message from the supplied input stream.
291 | * This message is NOT validated.
292 | *
293 | * @param in InputStream object to read data from
294 | * @return An AdbMessage object represented the message read
295 | * @throws IOException If the stream fails while reading
296 | */
297 | public static AdbMessage parseAdbMessage(InputStream in) throws IOException {
298 | AdbMessage msg = new AdbMessage();
299 | ByteBuffer packet = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
300 |
301 | /* Read the header first */
302 | int dataRead = 0;
303 | do {
304 | int bytesRead = in.read(packet.array(), dataRead, 24 - dataRead);
305 |
306 | if (bytesRead < 0)
307 | throw new IOException("Stream closed");
308 | else
309 | dataRead += bytesRead;
310 | }
311 | while (dataRead < ADB_HEADER_LENGTH);
312 |
313 | /* Pull out header fields */
314 | msg.command = packet.getInt();
315 | msg.arg0 = packet.getInt();
316 | msg.arg1 = packet.getInt();
317 | msg.payloadLength = packet.getInt();
318 | msg.checksum = packet.getInt();
319 | msg.magic = packet.getInt();
320 |
321 | /* If there's a payload supplied, read that too */
322 | if (msg.payloadLength != 0) {
323 | msg.payload = new byte[msg.payloadLength];
324 |
325 | dataRead = 0;
326 | do {
327 | int bytesRead = in.read(msg.payload, dataRead, msg.payloadLength - dataRead);
328 |
329 | if (bytesRead < 0)
330 | throw new IOException("Stream closed");
331 | else
332 | dataRead += bytesRead;
333 | }
334 | while (dataRead < msg.payloadLength);
335 | }
336 |
337 | return msg;
338 | }
339 | }
340 |
341 | }
342 |
--------------------------------------------------------------------------------
/src/main/java/com/tananaev/adblib/AdbStream.java:
--------------------------------------------------------------------------------
1 | package com.tananaev.adblib;
2 |
3 | import java.io.Closeable;
4 | import java.io.IOException;
5 | import java.util.Queue;
6 | import java.util.concurrent.ConcurrentLinkedQueue;
7 | import java.util.concurrent.atomic.AtomicBoolean;
8 |
9 | /**
10 | * This class abstracts the underlying ADB streams
11 | *
12 | * @author Cameron Gutman
13 | */
14 | public class AdbStream implements Closeable {
15 |
16 | /**
17 | * The AdbConnection object that the stream communicates over
18 | */
19 | private final AdbConnection adbConn;
20 |
21 | /**
22 | * The local ID of the stream
23 | */
24 | private final int localId;
25 |
26 | /**
27 | * The remote ID of the stream
28 | */
29 | private volatile int remoteId;
30 |
31 | /**
32 | * Indicates whether a write is currently allowed
33 | */
34 | private final AtomicBoolean writeReady;
35 |
36 | /**
37 | * A queue of data from the target's write packets
38 | */
39 | private final Queue readQueue;
40 |
41 | /**
42 | * Indicates whether the connection is closed already
43 | */
44 | private volatile boolean isClosed;
45 |
46 | /**
47 | * Whether the remote peer has closed but we still have unread data in the queue
48 | */
49 | private volatile boolean pendingClose;
50 |
51 | /**
52 | * Creates a new AdbStream object on the specified AdbConnection
53 | * with the given local ID.
54 | *
55 | * @param adbConn AdbConnection that this stream is running on
56 | * @param localId Local ID of the stream
57 | */
58 | public AdbStream(AdbConnection adbConn, int localId) {
59 | this.adbConn = adbConn;
60 | this.localId = localId;
61 | this.readQueue = new ConcurrentLinkedQueue();
62 | this.writeReady = new AtomicBoolean(false);
63 | this.isClosed = false;
64 | }
65 |
66 | /**
67 | * Called by the connection thread to indicate newly received data.
68 | *
69 | * @param payload Data inside the write message
70 | */
71 | void addPayload(byte[] payload) {
72 | synchronized (readQueue) {
73 | readQueue.add(payload);
74 | readQueue.notifyAll();
75 | }
76 | }
77 |
78 | /**
79 | * Called by the connection thread to send an OKAY packet, allowing the
80 | * other side to continue transmission.
81 | *
82 | * @throws IOException If the connection fails while sending the packet
83 | */
84 | void sendReady() throws IOException {
85 | /* Generate and send a READY packet */
86 | byte[] packet = AdbProtocol.generateReady(localId, remoteId);
87 |
88 | synchronized (adbConn.outputStream) {
89 | adbConn.outputStream.write(packet);
90 | adbConn.outputStream.flush();
91 | }
92 | }
93 |
94 | /**
95 | * Called by the connection thread to update the remote ID for this stream
96 | *
97 | * @param remoteId New remote ID
98 | */
99 | void updateRemoteId(int remoteId) {
100 | this.remoteId = remoteId;
101 | }
102 |
103 | /**
104 | * Called by the connection thread to indicate the stream is okay to send data.
105 | */
106 | void readyForWrite() {
107 | writeReady.set(true);
108 | }
109 |
110 | /**
111 | * Called by the connection thread to notify that the stream was closed by the peer.
112 | */
113 | void notifyClose(boolean closedByPeer) {
114 | /* We don't call close() because it sends another CLOSE */
115 | if (closedByPeer && !readQueue.isEmpty()) {
116 | /* The remote peer closed the stream but we haven't finished reading the remaining data */
117 | pendingClose = true;
118 | } else {
119 | isClosed = true;
120 | }
121 |
122 | /* Unwait readers and writers */
123 | synchronized (this) {
124 | notifyAll();
125 | }
126 | synchronized (readQueue) {
127 | readQueue.notifyAll();
128 | }
129 | }
130 |
131 | /**
132 | * Reads a pending write payload from the other side.
133 | *
134 | * @return Byte array containing the payload of the write
135 | * @throws InterruptedException If we are unable to wait for data
136 | * @throws IOException If the stream fails while waiting
137 | */
138 | public byte[] read() throws InterruptedException, IOException {
139 | byte[] data = null;
140 |
141 | synchronized (readQueue) {
142 | /* Wait for the connection to close or data to be received */
143 | while ((data = readQueue.poll()) == null && !isClosed) {
144 | readQueue.wait();
145 | }
146 |
147 | if (isClosed) {
148 | throw new IOException("Stream closed");
149 | }
150 |
151 | if (pendingClose && readQueue.isEmpty()) {
152 | /* The peer closed the stream, and we've finished reading the stream data, so this stream is finished */
153 | isClosed = true;
154 | }
155 | }
156 |
157 | return data;
158 | }
159 |
160 | /**
161 | * Sends a write packet with a given String payload.
162 | *
163 | * @param payload Payload in the form of a String
164 | * @throws IOException If the stream fails while sending data
165 | * @throws InterruptedException If we are unable to wait to send data
166 | */
167 | public void write(String payload) throws IOException, InterruptedException {
168 | /* ADB needs null-terminated strings */
169 | write(payload.getBytes("UTF-8"), false);
170 | write(new byte[]{0}, true);
171 | }
172 |
173 | /**
174 | * Sends a write packet with a given byte array payload.
175 | *
176 | * @param payload Payload in the form of a byte array
177 | * @throws IOException If the stream fails while sending data
178 | * @throws InterruptedException If we are unable to wait to send data
179 | */
180 | public void write(byte[] payload) throws IOException, InterruptedException {
181 | write(payload, true);
182 | }
183 |
184 | /**
185 | * Queues a write packet and optionally sends it immediately.
186 | *
187 | * @param payload Payload in the form of a byte array
188 | * @param flush Specifies whether to send the packet immediately
189 | * @throws IOException If the stream fails while sending data
190 | * @throws InterruptedException If we are unable to wait to send data
191 | */
192 | public void write(byte[] payload, boolean flush) throws IOException, InterruptedException {
193 | synchronized (this) {
194 | /* Make sure we're ready for a write */
195 | while (!isClosed && !writeReady.compareAndSet(true, false))
196 | wait();
197 |
198 | if (isClosed) {
199 | throw new IOException("Stream closed");
200 | }
201 | }
202 |
203 | /* Generate a WRITE packet and send it */
204 | byte[] packet = AdbProtocol.generateWrite(localId, remoteId, payload);
205 |
206 | synchronized (adbConn.outputStream) {
207 | adbConn.outputStream.write(packet);
208 |
209 | if (flush)
210 | adbConn.outputStream.flush();
211 | }
212 | }
213 |
214 | /**
215 | * Closes the stream. This sends a close message to the peer.
216 | *
217 | * @throws IOException If the stream fails while sending the close message.
218 | */
219 | @Override
220 | public void close() throws IOException {
221 | synchronized (this) {
222 | /* This may already be closed by the remote host */
223 | if (isClosed)
224 | return;
225 |
226 | /* Notify readers/writers that we've closed */
227 | notifyClose(false);
228 | }
229 |
230 | byte[] packet = AdbProtocol.generateClose(localId, remoteId);
231 |
232 | synchronized (adbConn.outputStream) {
233 | adbConn.outputStream.write(packet);
234 | adbConn.outputStream.flush();
235 | }
236 | }
237 |
238 | /**
239 | * Retreives whether the stream is closed or not
240 | *
241 | * @return True if the stream is close, false if not
242 | */
243 | public boolean isClosed() {
244 | return isClosed;
245 | }
246 |
247 | }
248 |
--------------------------------------------------------------------------------
/src/test/java/com/tananaev/adblib/AdbConnectionTest.java:
--------------------------------------------------------------------------------
1 | package com.tananaev.adblib;
2 |
3 | import org.junit.Test;
4 |
5 | import javax.xml.bind.DatatypeConverter;
6 | import java.net.Socket;
7 |
8 | public class AdbConnectionTest {
9 |
10 | //@Test
11 | public void testConnection() throws Exception {
12 |
13 | Socket socket = new Socket("192.168.1.15", 5555);
14 |
15 | AdbCrypto crypto = AdbCrypto.generateAdbKeyPair(new AdbBase64() {
16 | @Override
17 | public String encodeToString(byte[] data) {
18 | return DatatypeConverter.printBase64Binary(data);
19 | }
20 | });
21 |
22 | AdbConnection connection = AdbConnection.create(socket, crypto);
23 |
24 | connection.connect();
25 |
26 | AdbStream stream = connection.open("shell");
27 |
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/test/java/com/tananaev/adblib/AdbStreamTest.java:
--------------------------------------------------------------------------------
1 | package com.tananaev.adblib;
2 |
3 | import org.junit.*;
4 |
5 | import javax.xml.bind.DatatypeConverter;
6 | import java.io.IOException;
7 | import java.net.Socket;
8 | import java.nio.charset.StandardCharsets;
9 | import java.util.concurrent.TimeUnit;
10 |
11 | @Ignore
12 | public class AdbStreamTest {
13 |
14 | private static AdbCrypto crypto;
15 |
16 | private Socket socket;
17 | private AdbConnection connection;
18 |
19 | @BeforeClass
20 | public static void beforeClass() throws Exception {
21 | crypto = AdbCrypto.generateAdbKeyPair(new AdbBase64() {
22 | @Override
23 | public String encodeToString(byte[] data) {
24 | return DatatypeConverter.printBase64Binary(data);
25 | }
26 | });
27 | }
28 |
29 | @Before
30 | public void beforeTest() throws Exception {
31 | socket = new Socket("192.168.1.103", 5555);
32 |
33 | try {
34 | connection = AdbConnection.create(socket, crypto);
35 | connection.connect(Long.MAX_VALUE, TimeUnit.MILLISECONDS, true);
36 | } catch (AdbAuthenticationFailedException e) {
37 | System.out.println("On the target device, check 'Always allow from this computer' and press Allow");
38 | connection = AdbConnection.create(socket, crypto);
39 | connection.connect();
40 | }
41 | }
42 |
43 | @After
44 | public void afterTest() throws Exception {
45 | connection.close();
46 | connection = null;
47 | socket.close();
48 | socket = null;
49 | }
50 |
51 | @Test
52 | public void deliversRemainingDataOnRemoteStreamClose() throws Exception {
53 | try (AdbStream stream = connection.open("shell:echo Hello world")) {
54 | Thread.sleep(1000); // Giving the peer time to send us the "close stream" message
55 | Assert.assertFalse("Stream showed as closed before we read the data", stream.isClosed());
56 | byte[] response = stream.read();
57 | String responseText = new String(response, StandardCharsets.UTF_8);
58 | Assert.assertEquals("Hello world", responseText.trim());
59 | }
60 | }
61 |
62 | @Test
63 | public void showsAsClosedOnRemoteStreamCloseWithoutPendingData() throws Exception {
64 | try (AdbStream stream = connection.open("shell:echo Hello world")) {
65 | // Emptying the stream read queue
66 | byte[] response = stream.read();
67 | String responseText = new String(response, StandardCharsets.UTF_8);
68 | Assert.assertEquals("Hello world", responseText.trim());
69 |
70 | // Waiting for close message to arrive
71 | Thread.sleep(1000);
72 |
73 | Assert.assertTrue("Stream doesn't show as closed after the peer closed it and we emptied the read queue", stream.isClosed());
74 | }
75 | }
76 |
77 | @Test
78 | public void immediatelyShowsAsClosedOnLocalStreamClose() throws Exception {
79 | AdbStream stream = connection.open("shell:"); // Starting empty shell so it won't self-close
80 | stream.write("echo Hello world");
81 | Thread.sleep(1000); // Giving the peer time to run the command and send the output back
82 | stream.close();
83 |
84 | Assert.assertTrue("Stream not showing as closed after we closed it", stream.isClosed());
85 | }
86 |
87 | @Test
88 | public void doesntDeliverRemainingDataOnLocalStreamClose() throws Exception {
89 | AdbStream stream = connection.open("shell:"); // Starting empty shell so it won't self-close
90 | stream.write("echo Hello world");
91 | Thread.sleep(1000); // Giving the peer time to run the command and send the output back
92 | stream.close();
93 |
94 | boolean receivedDataAfterClose;
95 | try {
96 | stream.read();
97 | receivedDataAfterClose = true;
98 | } catch (IOException ignored) {
99 | receivedDataAfterClose = false;
100 | }
101 |
102 | Assert.assertFalse("Received data after we closed the stream", receivedDataAfterClose);
103 | }
104 |
105 | @Test
106 | public void showsAsClosedWhenClosedByPeerWhileEmpty() throws Exception {
107 | AdbStream stream = connection.open("shell: Hello world");
108 | stream.read(); // Emptying the stream
109 | Thread.sleep(1000); // Allowing time for the peer to send the close message
110 | Assert.assertTrue("Empty stream not showing as closed after close message received", stream.isClosed());
111 | }
112 |
113 | }
114 |
--------------------------------------------------------------------------------