├── .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 | --------------------------------------------------------------------------------