├── .idea ├── .name ├── copyright │ └── profiles_settings.xml ├── scopes │ └── scope_settings.xml ├── dictionaries │ └── vidstige.xml ├── vcs.xml ├── libraries │ └── junit_4_10.xml ├── modules.xml ├── encodings.xml ├── artifacts │ └── jadb_jar.xml └── runConfigurations │ └── MockedTestCases.xml ├── META-INF └── MANIFEST.MF ├── lib └── junit-4.10.jar ├── test ├── data │ └── Tiniest Smallest APK ever.apk └── se │ └── vidstige │ └── jadb │ ├── managers │ └── BashTest.java │ ├── test │ ├── fakes │ │ ├── FakeProcess.java │ │ ├── FakeSubprocess.java │ │ └── FakeAdbServer.java │ ├── unit │ │ ├── AdbOutputStreamFixture.java │ │ ├── AdbServerLauncherFixture.java │ │ ├── AdbInputStreamFixture.java │ │ ├── PackageManagerTest.java │ │ ├── PropertyManagerTest.java │ │ └── MockedTestCases.java │ └── integration │ │ ├── PackageManagerTests.java │ │ ├── ExecuteCmdTests.java │ │ └── RealDeviceTestCases.java │ ├── HostDisconnectFromRemoteTcpDeviceTest.java │ └── HostConnectToRemoteTcpDeviceTest.java ├── src └── se │ └── vidstige │ └── jadb │ ├── DeviceDetectionListener.java │ ├── ConnectionToRemoteDeviceException.java │ ├── ITransportFactory.java │ ├── Subprocess.java │ ├── JadbException.java │ ├── managers │ ├── Bash.java │ ├── Package.java │ ├── PropertyManager.java │ └── PackageManager.java │ ├── server │ ├── AdbResponder.java │ ├── AdbServer.java │ ├── AdbDeviceResponder.java │ ├── SocketServer.java │ └── AdbProtocolHandler.java │ ├── AdbFilterOutputStream.java │ ├── Stream.java │ ├── RemoteFileRecord.java │ ├── RemoteFile.java │ ├── HostConnectToRemoteTcpDevice.java │ ├── HostDisconnectFromRemoteTcpDevice.java │ ├── LookBackFilteringOutputStream.java │ ├── DeviceWatcher.java │ ├── AdbFilterInputStream.java │ ├── AdbServerLauncher.java │ ├── HostConnectionCommand.java │ ├── Transport.java │ ├── JadbConnection.java │ ├── ShellProcess.java │ ├── SyncTransport.java │ ├── ShellProtocolTransport.java │ ├── ShellProcessBuilder.java │ └── JadbDevice.java ├── .gitattributes ├── release.sh ├── .github └── workflows │ └── maven.yml ├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── pom.xml └── LICENSE.md /.idea/.name: -------------------------------------------------------------------------------- 1 | jadb -------------------------------------------------------------------------------- /META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | 3 | -------------------------------------------------------------------------------- /lib/junit-4.10.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReVanced/jadb/HEAD/lib/junit-4.10.jar -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/data/Tiniest Smallest APK ever.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReVanced/jadb/HEAD/test/data/Tiniest Smallest APK ever.apk -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/dictionaries/vidstige.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jadb 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/libraries/junit_4_10.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/DeviceDetectionListener.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.util.List; 4 | 5 | public interface DeviceDetectionListener { 6 | void onDetect(List devices); 7 | void onException(Exception e); 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/ConnectionToRemoteDeviceException.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | public class ConnectionToRemoteDeviceException extends Exception { 4 | public ConnectionToRemoteDeviceException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/ITransportFactory.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Created by Törcsi on 2016. 03. 01.. 7 | */ 8 | public interface ITransportFactory { 9 | Transport createTransport() throws IOException; 10 | } 11 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/Subprocess.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.IOException; 4 | 5 | public class Subprocess { 6 | public Process execute(String[] command) throws IOException { 7 | return Runtime.getRuntime().exec(command); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/JadbException.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | public class JadbException extends Exception { 4 | 5 | public JadbException(String message) { 6 | super(message); 7 | } 8 | 9 | private static final long serialVersionUID = -3879283786835654165L; 10 | } 11 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/managers/Bash.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.managers; 2 | 3 | public class Bash { 4 | private Bash() { 5 | throw new IllegalStateException("Utility class"); 6 | } 7 | 8 | public static String quote(String s) { 9 | return "'" + s.replace("'", "'\\''") + "'"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/server/AdbResponder.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.server; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Created by vidstige on 20/03/14. 7 | */ 8 | public interface AdbResponder { 9 | void onCommand(String command); 10 | 11 | int getVersion(); 12 | 13 | List getDevices(); 14 | } 15 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/managers/BashTest.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.managers; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class BashTest { 8 | 9 | @Test 10 | public void quote() { 11 | // http://wiki.bash-hackers.org/syntax/quoting#strong_quoting 12 | assertEquals("'-t '\\''aaa'\\'''", Bash.quote("-t 'aaa'")); 13 | } 14 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Standard to msysgit 5 | *.doc diff=astextplain 6 | *.DOC diff=astextplain 7 | *.docx diff=astextplain 8 | *.DOCX diff=astextplain 9 | *.dot diff=astextplain 10 | *.DOT diff=astextplain 11 | *.pdf diff=astextplain 12 | *.PDF diff=astextplain 13 | *.rtf diff=astextplain 14 | *.RTF diff=astextplain 15 | -------------------------------------------------------------------------------- /.idea/artifacts/jadb_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/out/artifacts/jadb_jar 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "Usage: $0 " 5 | exit -1 6 | fi 7 | 8 | if [ ! -f access_token ]; then 9 | echo "Place your access token in a file called access_token" 10 | exit -2 11 | fi 12 | 13 | VERSION=$1 14 | ACCESS_TOKEN=$( list(String path) throws IOException; 23 | } 24 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/Stream.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.nio.charset.Charset; 8 | 9 | public class Stream { 10 | private Stream() { 11 | throw new IllegalStateException("Utility class"); 12 | } 13 | 14 | public static void copy(InputStream in, OutputStream out) throws IOException { 15 | byte[] buffer = new byte[1024 * 10]; 16 | int len; 17 | while ((len = in.read(buffer)) != -1) { 18 | out.write(buffer, 0, len); 19 | } 20 | } 21 | 22 | public static String readAll(InputStream input, Charset charset) throws IOException { 23 | ByteArrayOutputStream tmp = new ByteArrayOutputStream(); 24 | Stream.copy(input, tmp); 25 | return new String(tmp.toByteArray(), charset); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/RemoteFileRecord.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | /** 4 | * Created by vidstige on 2014-03-19. 5 | */ 6 | class RemoteFileRecord extends RemoteFile { 7 | public static final RemoteFileRecord DONE = new RemoteFileRecord(null, 0, 0, 0); 8 | 9 | private final int mode; 10 | private final int size; 11 | private final int lastModified; 12 | 13 | public RemoteFileRecord(String name, int mode, int size, int lastModified) { 14 | super(name); 15 | this.mode = mode; 16 | this.size = size; 17 | this.lastModified = lastModified; 18 | } 19 | 20 | @Override 21 | public int getSize() { 22 | return size; 23 | } 24 | 25 | @Override 26 | public int getLastModified() { 27 | return lastModified; 28 | } 29 | 30 | @Override 31 | public boolean isDirectory() { 32 | return (mode & (1 << 14)) == (1 << 14); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/fakes/FakeProcess.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.fakes; 2 | 3 | import java.io.InputStream; 4 | import java.io.OutputStream; 5 | 6 | public class FakeProcess extends Process { 7 | private final int exitValue; 8 | 9 | public FakeProcess(int exitValue) { 10 | this.exitValue = exitValue; 11 | } 12 | 13 | @Override 14 | public OutputStream getOutputStream() { 15 | return null; 16 | } 17 | 18 | @Override 19 | public InputStream getInputStream() { 20 | return null; 21 | } 22 | 23 | @Override 24 | public InputStream getErrorStream() { 25 | return null; 26 | } 27 | 28 | @Override 29 | public int waitFor() throws InterruptedException { 30 | return 0; 31 | } 32 | 33 | @Override 34 | public int exitValue() { 35 | return exitValue; 36 | } 37 | 38 | @Override 39 | public void destroy() { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/RemoteFile.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | /** 4 | * Created by vidstige on 2014-03-20 5 | */ 6 | public class RemoteFile { 7 | private final String path; 8 | 9 | public RemoteFile(String path) { this.path = path; } 10 | 11 | public String getName() { throw new UnsupportedOperationException(); } 12 | public int getSize() { throw new UnsupportedOperationException(); } 13 | public int getLastModified() { throw new UnsupportedOperationException(); } 14 | public boolean isDirectory() { throw new UnsupportedOperationException(); } 15 | 16 | public String getPath() { return path;} 17 | 18 | @Override 19 | public boolean equals(Object o) { 20 | if (this == o) return true; 21 | if (o == null || getClass() != o.getClass()) return false; 22 | 23 | RemoteFile that = (RemoteFile) o; 24 | return path.equals(that.path); 25 | } 26 | 27 | @Override 28 | public int hashCode() { 29 | return path.hashCode(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Used to store github access token for releasing 2 | /access_token 3 | 4 | ################ 5 | # IntelliJ # 6 | ################ 7 | /.idea/* 8 | jadb.iml 9 | 10 | 11 | ############# 12 | # Maven # 13 | ############# 14 | out/ 15 | target/ 16 | 17 | 18 | ################# 19 | ## Eclipse 20 | ################# 21 | 22 | *.pydevproject 23 | #.project 24 | .metadata 25 | bin/ 26 | tmp/ 27 | *.tmp 28 | *.bak 29 | *.swp 30 | *~.nib 31 | local.properties 32 | #.classpath 33 | .settings/ 34 | .loadpath 35 | 36 | # External tool builders 37 | .externalToolBuilders/ 38 | 39 | # Locally stored "Eclipse launch configurations" 40 | *.launch 41 | 42 | # CDT-specific 43 | .cproject 44 | 45 | # PDT-specific 46 | .buildpath 47 | 48 | 49 | ##################### 50 | ## Windows detritus # 51 | ##################### 52 | 53 | # Windows image file caches 54 | Thumbs.db 55 | ehthumbs.db 56 | 57 | # Folder config file 58 | Desktop.ini 59 | 60 | # Recycle Bin used on file shares 61 | $RECYCLE.BIN/ 62 | 63 | ################# 64 | ## Mac detritus # 65 | ################# 66 | .DS_Store 67 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/HostConnectToRemoteTcpDevice.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.IOException; 4 | import java.net.InetSocketAddress; 5 | 6 | class HostConnectToRemoteTcpDevice extends HostConnectionCommand { 7 | HostConnectToRemoteTcpDevice(Transport transport) { 8 | super(transport, new ResponseValidatorImp()); 9 | } 10 | 11 | //Visible for testing 12 | HostConnectToRemoteTcpDevice(Transport transport, ResponseValidator responseValidator) { 13 | super(transport, responseValidator); 14 | } 15 | 16 | InetSocketAddress connect(InetSocketAddress inetSocketAddress) 17 | throws IOException, JadbException, ConnectionToRemoteDeviceException { 18 | return executeHostCommand("connect", inetSocketAddress); 19 | } 20 | 21 | static final class ResponseValidatorImp extends ResponseValidatorBase { 22 | private static final String SUCCESSFULLY_CONNECTED = "connected to"; 23 | private static final String ALREADY_CONNECTED = "already connected to"; 24 | 25 | ResponseValidatorImp() { 26 | super(SUCCESSFULLY_CONNECTED, ALREADY_CONNECTED); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/HostDisconnectFromRemoteTcpDevice.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.IOException; 4 | import java.net.InetSocketAddress; 5 | 6 | public class HostDisconnectFromRemoteTcpDevice extends HostConnectionCommand { 7 | HostDisconnectFromRemoteTcpDevice(Transport transport) { 8 | super(transport, new ResponseValidatorImp()); 9 | 10 | } 11 | 12 | //Visible for testing 13 | HostDisconnectFromRemoteTcpDevice(Transport transport, ResponseValidator responseValidator) { 14 | super(transport, responseValidator); 15 | } 16 | 17 | InetSocketAddress disconnect(InetSocketAddress inetSocketAddress) 18 | throws IOException, JadbException, ConnectionToRemoteDeviceException { 19 | return executeHostCommand("disconnect", inetSocketAddress); 20 | } 21 | 22 | static final class ResponseValidatorImp extends ResponseValidatorBase { 23 | private static final String SUCCESSFULLY_DISCONNECTED = "disconnected"; 24 | private static final String ALREADY_DISCONNECTED = "error: no such device"; 25 | 26 | ResponseValidatorImp() { 27 | super(SUCCESSFULLY_DISCONNECTED, ALREADY_DISCONNECTED); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.idea/runConfigurations/MockedTestCases.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/LookBackFilteringOutputStream.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.FilterOutputStream; 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | import java.util.ArrayDeque; 7 | 8 | public class LookBackFilteringOutputStream extends FilterOutputStream { 9 | private final ArrayDeque buffer; 10 | private final int lookBackBufferSize; 11 | 12 | protected LookBackFilteringOutputStream(OutputStream inner, int lookBackBufferSize) 13 | { 14 | super(inner); 15 | this.lookBackBufferSize = lookBackBufferSize; 16 | this.buffer = new ArrayDeque<>(lookBackBufferSize); 17 | } 18 | 19 | protected void unwrite() { 20 | buffer.removeFirst(); 21 | } 22 | 23 | protected ArrayDeque lookback() { 24 | return buffer; 25 | } 26 | 27 | @Override 28 | public void write(int c) throws IOException { 29 | buffer.addLast((byte) c); 30 | flushBuffer(lookBackBufferSize); 31 | } 32 | 33 | @Override 34 | public void flush() throws IOException { 35 | flushBuffer(0); 36 | out.flush(); 37 | } 38 | 39 | private void flushBuffer(int size) throws IOException { 40 | while (buffer.size() > size) { 41 | Byte b = buffer.removeFirst(); 42 | out.write(b); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/DeviceWatcher.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.IOException; 4 | 5 | public class DeviceWatcher implements Runnable { 6 | private Transport transport; 7 | private final DeviceDetectionListener listener; 8 | private final JadbConnection connection; 9 | 10 | public DeviceWatcher(Transport transport, DeviceDetectionListener listener, JadbConnection connection) { 11 | this.transport = transport; 12 | this.listener = listener; 13 | this.connection = connection; 14 | } 15 | 16 | @Override 17 | public void run() { 18 | watch(); 19 | } 20 | 21 | @SuppressWarnings("squid:S2189") // watcher is stopped by closing transport 22 | private void watch() { 23 | try { 24 | while (true) { 25 | listener.onDetect(connection.parseDevices(transport.readString())); 26 | } 27 | } catch (IOException ioe) { 28 | synchronized(this) { 29 | if (transport != null) { 30 | listener.onException(ioe); 31 | } 32 | } 33 | } catch (Exception e) { 34 | listener.onException(e); 35 | } 36 | } 37 | 38 | public void stop() throws IOException { 39 | synchronized(this) { 40 | transport.close(); 41 | transport = null; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/AdbFilterInputStream.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.FilterInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | 7 | public class AdbFilterInputStream extends FilterInputStream { 8 | public AdbFilterInputStream(InputStream inputStream) { 9 | super(inputStream); 10 | } 11 | 12 | @Override 13 | public int read() throws IOException { 14 | int b1 = in.read(); 15 | if (b1 == 0x0d) { 16 | in.mark(1); 17 | int b2 = in.read(); 18 | if (b2 == 0x0a) { 19 | return b2; 20 | } 21 | in.reset(); 22 | } 23 | return b1; 24 | } 25 | 26 | @Override 27 | public int read(byte[] buffer, int offset, int length) throws IOException { 28 | int n = 0; 29 | for (int i = 0; i < length; i++) { 30 | int b = read(); 31 | if (b == -1) return n == 0 ? -1 : n; 32 | buffer[offset + n] = (byte) b; 33 | n++; 34 | 35 | // Return as soon as no more data is available (and at least one byte was read) 36 | if (in.available() <= 0) { 37 | return n; 38 | } 39 | } 40 | return n; 41 | } 42 | 43 | @Override 44 | public int read(byte[] buffer) throws IOException { 45 | return read(buffer, 0, buffer.length); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing # 2 | ## Why contributing? ## 3 | The original author and all users of this library are very grateful for your contribution 4 | to this Open Source Project. Also most employers value people active in the Open Source 5 | community. 6 | 7 | ## The Checklist ## 8 | If you want to help out and contribute to this Open Source Project, please keep reading. 9 | 10 | My thought with `jadb` was to make it very light weight and easy to use as a developer. 11 | This means as little boilerplate code, (mostly) self-documenting public interface, and 12 | overall clean code. 13 | 14 | Before submitting a pull request, please go through the below checklist to verify 15 | your proposed change meets, or exceeds, the quality of the jadb source code. 16 | 17 | * Builds - Make sure the code builds by issuing `mvn clean install test`. 18 | * Works - Make sure all the test runs and passes. 19 | * Works - Double check any features you might have changed, and of course any _new_ code 20 | by testing manually. 21 | * Formatting - Keep the formatting _consistent_. Nothing 22 | fancy, pretty standard java stuff, check other source files 23 | for reference. 24 | * Readability - Is your code easy to read? This usually means shorter code, but don't go 25 | full terse. 26 | * Newline at end of file - This makes `cat`-ing files, etc easier. 27 | 28 | Happy coding! I, the original author, and all users are grateful for your contribution. :-) 29 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/unit/AdbOutputStreamFixture.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.unit; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import se.vidstige.jadb.AdbFilterOutputStream; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.IOException; 9 | import java.io.OutputStream; 10 | 11 | public class AdbOutputStreamFixture { 12 | 13 | private byte[] passthrough(byte[] input) throws IOException { 14 | ByteArrayOutputStream output = new ByteArrayOutputStream(); 15 | try (OutputStream sut = new AdbFilterOutputStream(output)) { 16 | sut.write(input); 17 | sut.flush(); 18 | } 19 | return output.toByteArray(); 20 | } 21 | 22 | @Test 23 | public void testSimple() throws Exception { 24 | byte[] actual = passthrough(new byte[]{ 1, 2, 3}); 25 | Assert.assertArrayEquals(new byte[]{ 1, 2, 3}, actual); 26 | } 27 | 28 | @Test 29 | public void testEmpty() throws Exception { 30 | byte[] actual = passthrough(new byte[]{}); 31 | Assert.assertArrayEquals(new byte[]{}, actual); 32 | } 33 | 34 | @Test 35 | public void testSimpleRemoval() throws Exception { 36 | byte[] actual = passthrough(new byte[]{0x0d, 0x0a}); 37 | Assert.assertArrayEquals(new byte[]{0x0a}, actual); 38 | } 39 | 40 | @Test 41 | public void testDoubleRemoval() throws Exception { 42 | byte[] actual = passthrough(new byte[]{0x0d, 0x0a, 0x0d, 0x0a}); 43 | Assert.assertArrayEquals(new byte[]{0x0a, 0x0a}, actual); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/unit/AdbServerLauncherFixture.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.unit; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import se.vidstige.jadb.AdbServerLauncher; 7 | import se.vidstige.jadb.test.fakes.FakeSubprocess; 8 | 9 | import java.io.IOException; 10 | import java.util.*; 11 | 12 | public class AdbServerLauncherFixture { 13 | 14 | private FakeSubprocess subprocess; 15 | private Map environment = new HashMap<>(); 16 | 17 | @Before 18 | public void setUp() { 19 | subprocess = new FakeSubprocess(); 20 | } 21 | @After 22 | public void tearDown() { 23 | subprocess.verifyExpectations(); 24 | } 25 | 26 | @Test 27 | public void testStartServer() throws Exception { 28 | subprocess.expect(new String[]{"/abc/platform-tools/adb", "start-server"}, 0); 29 | Map environment = new HashMap<>(); 30 | environment.put("ANDROID_HOME", "/abc"); 31 | new AdbServerLauncher(subprocess, environment).launch(); 32 | } 33 | 34 | @Test 35 | public void testStartServerWithoutANDROID_HOME() throws Exception { 36 | subprocess.expect(new String[]{"adb", "start-server"}, 0); 37 | new AdbServerLauncher(subprocess, environment).launch(); 38 | } 39 | 40 | @Test(expected=IOException.class) 41 | public void testStartServerFails() throws Exception { 42 | subprocess.expect(new String[]{"adb", "start-server"}, -1); 43 | new AdbServerLauncher(subprocess, environment).launch(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/unit/AdbInputStreamFixture.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.unit; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import se.vidstige.jadb.AdbFilterInputStream; 6 | import se.vidstige.jadb.Stream; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | 13 | public class AdbInputStreamFixture { 14 | 15 | private byte[] passthrough(byte[] input) throws IOException { 16 | ByteArrayInputStream inputStream = new ByteArrayInputStream(input); 17 | InputStream sut = new AdbFilterInputStream(inputStream); 18 | ByteArrayOutputStream output = new ByteArrayOutputStream(); 19 | Stream.copy(sut, output); 20 | return output.toByteArray(); 21 | } 22 | 23 | @Test 24 | public void testSimple() throws Exception { 25 | byte[] actual = passthrough(new byte[]{ 1, 2, 3}); 26 | Assert.assertArrayEquals(new byte[]{ 1, 2, 3}, actual); 27 | } 28 | 29 | @Test 30 | public void testEmpty() throws Exception { 31 | byte[] actual = passthrough(new byte[]{}); 32 | Assert.assertArrayEquals(new byte[]{}, actual); 33 | } 34 | 35 | @Test 36 | public void testSimpleRemoval() throws Exception { 37 | byte[] actual = passthrough(new byte[]{0x0d, 0x0a}); 38 | Assert.assertArrayEquals(new byte[]{0x0a}, actual); 39 | } 40 | 41 | @Test 42 | public void testDoubleRemoval() throws Exception { 43 | byte[] actual = passthrough(new byte[]{0x0d, 0x0a, 0x0d, 0x0a}); 44 | Assert.assertArrayEquals(new byte[]{0x0a, 0x0a}, actual); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/AdbServerLauncher.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.IOException; 4 | import java.util.Map; 5 | 6 | /** 7 | * Launches the ADB server 8 | */ 9 | public class AdbServerLauncher { 10 | private final String executable; 11 | private Subprocess subprocess; 12 | 13 | /** 14 | * Creates a new launcher loading ADB from the environment. 15 | * 16 | * @param subprocess the sub-process. 17 | * @param environment the environment to use to locate the ADB executable. 18 | */ 19 | public AdbServerLauncher(Subprocess subprocess, Map environment) { 20 | this(subprocess, findAdbExecutable(environment)); 21 | } 22 | 23 | /** 24 | * Creates a new launcher with the specified ADB. 25 | * 26 | * @param subprocess the sub-process. 27 | * @param executable the location of the ADB executable. 28 | */ 29 | public AdbServerLauncher(Subprocess subprocess, String executable) { 30 | this.subprocess = subprocess; 31 | this.executable = executable; 32 | } 33 | 34 | private static String findAdbExecutable(Map environment) { 35 | String androidHome = environment.get("ANDROID_HOME"); 36 | if (androidHome == null || androidHome.equals("")) { 37 | return "adb"; 38 | } 39 | return androidHome + "/platform-tools/adb"; 40 | } 41 | 42 | public void launch() throws IOException, InterruptedException { 43 | Process p = subprocess.execute(new String[]{executable, "start-server"}); 44 | p.waitFor(); 45 | int exitValue = p.exitValue(); 46 | if (exitValue != 0) throw new IOException("adb exited with exit code: " + exitValue); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/managers/PropertyManager.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.managers; 2 | 3 | import se.vidstige.jadb.JadbDevice; 4 | import se.vidstige.jadb.JadbException; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.IOException; 8 | import java.io.InputStreamReader; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.regex.Matcher; 13 | import java.util.regex.Pattern; 14 | 15 | /** 16 | * A class which works with properties, uses getprop and setprop methods of android shell 17 | */ 18 | public class PropertyManager { 19 | private final Pattern pattern = Pattern.compile("^\\[([a-zA-Z0-9_.-]*)]:.\\[([^\\[\\]]*)]"); 20 | private final JadbDevice device; 21 | 22 | public PropertyManager(JadbDevice device) { 23 | this.device = device; 24 | } 25 | 26 | public Map getprop() throws IOException, JadbException { 27 | try (BufferedReader bufferedReader = 28 | new BufferedReader(new InputStreamReader(device.executeShell("getprop"), StandardCharsets.UTF_8))) { 29 | return parseProp(bufferedReader); 30 | } 31 | } 32 | 33 | private Map parseProp(BufferedReader bufferedReader) throws IOException { 34 | HashMap result = new HashMap<>(); 35 | 36 | String line; 37 | Matcher matcher = pattern.matcher(""); 38 | 39 | while ((line = bufferedReader.readLine()) != null) { 40 | matcher.reset(line); 41 | 42 | if (matcher.find()) { 43 | if (matcher.groupCount() < 2) { 44 | System.err.println("Property line: " + line + " does not match pattern. Ignoring"); 45 | continue; 46 | } 47 | String key = matcher.group(1); 48 | String value = matcher.group(2); 49 | result.put(key, value); 50 | } 51 | } 52 | 53 | return result; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/server/SocketServer.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.server; 2 | 3 | import java.io.IOException; 4 | import java.net.ServerSocket; 5 | import java.net.Socket; 6 | 7 | // >set ANDROID_ADB_SERVER_PORT=15037 8 | public abstract class SocketServer implements Runnable { 9 | 10 | private final int port; 11 | private ServerSocket socket; 12 | private Thread thread; 13 | 14 | private boolean isStarted = false; 15 | private final Object lockObject = new Object(); 16 | 17 | protected SocketServer(int port) { 18 | this.port = port; 19 | } 20 | 21 | public void start() throws InterruptedException { 22 | thread = new Thread(this, "Fake Adb Server"); 23 | thread.setDaemon(true); 24 | thread.start(); 25 | waitForServer(); 26 | } 27 | 28 | public int getPort() { 29 | return port; 30 | } 31 | 32 | @SuppressWarnings("squid:S2189") // server is stopped by closing SocketServer 33 | @Override 34 | public void run() { 35 | try { 36 | socket = new ServerSocket(port); 37 | socket.setReuseAddress(true); 38 | 39 | serverReady(); 40 | 41 | while (true) { 42 | Socket c = socket.accept(); 43 | createResponder(c).run(); 44 | } 45 | } catch (IOException e) { 46 | // Empty on purpose 47 | } 48 | } 49 | 50 | private void serverReady() { 51 | synchronized (lockObject) { 52 | isStarted = true; 53 | lockObject.notifyAll(); 54 | } 55 | } 56 | 57 | private void waitForServer() throws InterruptedException { 58 | synchronized (lockObject) { 59 | while (!isStarted) { 60 | lockObject.wait(); 61 | } 62 | } 63 | } 64 | 65 | protected abstract Runnable createResponder(Socket socket); 66 | 67 | public void stop() throws IOException, InterruptedException { 68 | socket.close(); 69 | thread.join(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/integration/PackageManagerTests.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.integration; 2 | 3 | import org.junit.Before; 4 | import org.junit.BeforeClass; 5 | import org.junit.Test; 6 | import se.vidstige.jadb.JadbConnection; 7 | import se.vidstige.jadb.managers.Package; 8 | import se.vidstige.jadb.managers.PackageManager; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | public class PackageManagerTests { 16 | private static JadbConnection jadb; 17 | private PackageManager pm; 18 | private final File miniApk = new File("test/data/Tiniest Smallest APK ever.apk"); 19 | 20 | @BeforeClass 21 | public static void connect() throws IOException { 22 | try { 23 | jadb = new JadbConnection(); 24 | jadb.getHostVersion(); 25 | } catch (Exception e) { 26 | org.junit.Assume.assumeNoException(e); 27 | } 28 | } 29 | 30 | @Before 31 | public void createPackageManager() 32 | { 33 | pm = new PackageManager(jadb.getAnyDevice()); 34 | } 35 | 36 | @Test 37 | public void testLaunchActivity() throws Exception { 38 | pm.launch(new Package("com.android.settings")); 39 | } 40 | 41 | @Test 42 | public void testListPackages() throws Exception { 43 | List packages = pm.getPackages(); 44 | for (Package p : packages) { 45 | System.out.println(p); 46 | } 47 | } 48 | 49 | @Test 50 | public void testInstallUninstallCycle() throws Exception { 51 | pm.install(miniApk); 52 | pm.forceInstall(miniApk); 53 | pm.uninstall(new Package("b.a")); 54 | } 55 | 56 | 57 | @Test 58 | public void testInstallWithOptionsUninstallCycle() throws Exception { 59 | pm.install(miniApk); 60 | pm.installWithOptions(miniApk, Arrays.asList(PackageManager.REINSTALL_KEEPING_DATA, PackageManager.ALLOW_VERSION_DOWNGRADE)); 61 | pm.uninstall(new Package("b.a")); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/integration/ExecuteCmdTests.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.integration; 2 | 3 | import org.junit.Assert; 4 | import org.junit.BeforeClass; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.junit.runners.Parameterized; 8 | import se.vidstige.jadb.JadbConnection; 9 | import se.vidstige.jadb.JadbDevice; 10 | import se.vidstige.jadb.Stream; 11 | 12 | import java.io.BufferedReader; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.InputStreamReader; 16 | import java.io.Reader; 17 | import java.nio.charset.Charset; 18 | import java.nio.charset.StandardCharsets; 19 | import java.util.Arrays; 20 | import java.util.Collection; 21 | 22 | @RunWith(Parameterized.class) 23 | public class ExecuteCmdTests { 24 | private static JadbConnection jadb; 25 | private static JadbDevice jadbDevice; 26 | 27 | @Parameterized.Parameter 28 | public String input; 29 | 30 | 31 | @Parameterized.Parameters(name="Test {index} input={0}") 32 | public static Collection input() { 33 | return Arrays.asList(new Object[]{ 34 | "öäasd", 35 | "asf dsa", 36 | "sdf&g", 37 | "sd& fg", 38 | "da~f", 39 | "asd'as", 40 | "a¡f", 41 | "asüd", 42 | "adös tz", 43 | "⾀", 44 | "å", 45 | "æ", 46 | "{}"}); 47 | } 48 | 49 | @BeforeClass 50 | public static void connect() { 51 | try { 52 | jadb = new JadbConnection(); 53 | jadb.getHostVersion(); 54 | jadbDevice = jadb.getAnyDevice(); 55 | } catch (Exception e) { 56 | org.junit.Assume.assumeNoException(e); 57 | } 58 | } 59 | 60 | 61 | @Test 62 | public void testExecuteWithSpecialChars() throws Exception { 63 | InputStream response = jadbDevice.execute("echo", input); 64 | Assert.assertEquals(input, Stream.readAll(response, StandardCharsets.UTF_8).replaceAll("\n$", "")); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/fakes/FakeSubprocess.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.fakes; 2 | 3 | import se.vidstige.jadb.Subprocess; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | public class FakeSubprocess extends Subprocess { 11 | private List expectations = new ArrayList<>(); 12 | 13 | public Expectation expect(String[] command, int exitValue) { 14 | Expectation builder = new Expectation(command, exitValue); 15 | expectations.add(builder); 16 | return builder; 17 | } 18 | 19 | private String format(String[] command) { 20 | StringBuilder sb = new StringBuilder(); 21 | for (int i = 0; i < command.length; i++) { 22 | if (i > 0) { 23 | sb.append(" "); 24 | } 25 | sb.append(command[i]); 26 | } 27 | return sb.toString(); 28 | } 29 | 30 | @Override 31 | public Process execute(String[] command) throws IOException { 32 | List toRemove = new ArrayList<>(); 33 | for (Expectation e : expectations) { 34 | if (e.matches(command)) { 35 | toRemove.add(e); 36 | } 37 | } 38 | expectations.removeAll(toRemove); 39 | if (toRemove.size() == 1) { 40 | return new FakeProcess(toRemove.get(0).getExitValue()); 41 | } 42 | throw new AssertionError("Unexpected command: " + format(command)); 43 | } 44 | 45 | public void verifyExpectations() { 46 | if (expectations.size() > 0) { 47 | throw new AssertionError("Subprocess never called: " + format(expectations.get(0).getCommand())); 48 | } 49 | } 50 | 51 | private static class Expectation { 52 | private final String[] command; 53 | private final int exitValue; 54 | 55 | public Expectation(String[] command, int exitValue) { 56 | this.command = command; 57 | this.exitValue = exitValue; 58 | } 59 | 60 | public boolean matches(String[] command) { 61 | return Arrays.equals(command, this.command); 62 | } 63 | 64 | public int getExitValue() { 65 | return exitValue; 66 | } 67 | 68 | public String[] getCommand() { 69 | return command; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/HostConnectionCommand.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.IOException; 4 | import java.net.InetSocketAddress; 5 | 6 | public class HostConnectionCommand { 7 | private final Transport transport; 8 | private final ResponseValidator responseValidator; 9 | 10 | HostConnectionCommand(Transport transport, ResponseValidator responseValidator) { 11 | this.transport = transport; 12 | this.responseValidator = responseValidator; 13 | } 14 | 15 | InetSocketAddress executeHostCommand(String command, InetSocketAddress inetSocketAddress) 16 | throws IOException, JadbException, ConnectionToRemoteDeviceException { 17 | transport.send(String.format("host:%s:%s:%d", command, inetSocketAddress.getHostString(), inetSocketAddress.getPort())); 18 | verifyTransportLevel(); 19 | verifyProtocolLevel(); 20 | 21 | return inetSocketAddress; 22 | } 23 | 24 | private void verifyTransportLevel() throws IOException, JadbException { 25 | transport.verifyResponse(); 26 | } 27 | 28 | private void verifyProtocolLevel() throws IOException, ConnectionToRemoteDeviceException { 29 | String status = transport.readString(); 30 | responseValidator.validate(status); 31 | } 32 | 33 | //@VisibleForTesting 34 | interface ResponseValidator { 35 | void validate(String response) throws ConnectionToRemoteDeviceException; 36 | } 37 | 38 | static class ResponseValidatorBase implements ResponseValidator { 39 | private final String successMessage; 40 | private final String errorMessage; 41 | 42 | ResponseValidatorBase(String successMessage, String errorMessage) { 43 | this.successMessage = successMessage; 44 | this.errorMessage = errorMessage; 45 | } 46 | 47 | public void validate(String response) throws ConnectionToRemoteDeviceException { 48 | if (!checkIfConnectedSuccessfully(response) && !checkIfAlreadyConnected(response)) { 49 | throw new ConnectionToRemoteDeviceException(extractError(response)); 50 | } 51 | } 52 | 53 | private boolean checkIfConnectedSuccessfully(String response) { 54 | return response.startsWith(successMessage); 55 | } 56 | 57 | private boolean checkIfAlreadyConnected(String response) { 58 | return response.startsWith(errorMessage); 59 | } 60 | 61 | private String extractError(String response) { 62 | int lastColon = response.lastIndexOf(':'); 63 | if (lastColon != -1) { 64 | return response.substring(lastColon); 65 | } else { 66 | return response; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/Transport.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.*; 4 | import java.net.Socket; 5 | import java.nio.charset.StandardCharsets; 6 | 7 | class Transport implements Closeable { 8 | 9 | private final OutputStream outputStream; 10 | private final InputStream inputStream; 11 | private final DataInputStream dataInput; 12 | private final DataOutputStream dataOutput; 13 | 14 | private Transport(OutputStream outputStream, InputStream inputStream) { 15 | this.outputStream = outputStream; 16 | this.inputStream = inputStream; 17 | this.dataInput = new DataInputStream(inputStream); 18 | this.dataOutput = new DataOutputStream(outputStream); 19 | } 20 | 21 | public Transport(Socket socket) throws IOException { 22 | this(socket.getOutputStream(), socket.getInputStream()); 23 | } 24 | 25 | public String readString() throws IOException { 26 | String encodedLength = readString(4); 27 | int length = Integer.parseInt(encodedLength, 16); 28 | return readString(length); 29 | } 30 | 31 | public void readResponseTo(OutputStream output) throws IOException { 32 | Stream.copy(inputStream, output); 33 | } 34 | 35 | public InputStream getInputStream() { 36 | return inputStream; 37 | } 38 | 39 | public void verifyResponse() throws IOException, JadbException { 40 | String response = readString(4); 41 | if (!"OKAY".equals(response)) { 42 | String error = readString(); 43 | throw new JadbException("command failed: " + error); 44 | } 45 | } 46 | 47 | public String readString(int length) throws IOException { 48 | byte[] responseBuffer = new byte[length]; 49 | dataInput.readFully(responseBuffer); 50 | return new String(responseBuffer, StandardCharsets.UTF_8); 51 | } 52 | 53 | private String getCommandLength(String command) { 54 | return String.format("%04x", command.getBytes().length); 55 | } 56 | 57 | public void send(String command) throws IOException { 58 | OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); 59 | writer.write(getCommandLength(command)); 60 | writer.write(command); 61 | writer.flush(); 62 | } 63 | 64 | public SyncTransport startSync() throws IOException, JadbException { 65 | send("sync:"); 66 | verifyResponse(); 67 | return new SyncTransport(dataOutput, dataInput); 68 | } 69 | 70 | public ShellProtocolTransport startShellProtocol(String command) throws IOException, JadbException { 71 | send("shell,v2,raw:" + command); 72 | verifyResponse(); 73 | return new ShellProtocolTransport(dataOutput, dataInput); 74 | } 75 | 76 | @Override 77 | public void close() throws IOException { 78 | dataInput.close(); 79 | dataOutput.close(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/JadbConnection.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | 4 | import java.io.IOException; 5 | import java.net.InetSocketAddress; 6 | import java.net.Socket; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class JadbConnection implements ITransportFactory { 11 | 12 | private final String host; 13 | private final int port; 14 | 15 | private static final int DEFAULTPORT = 5037; 16 | 17 | public JadbConnection() { 18 | this("localhost", DEFAULTPORT); 19 | } 20 | 21 | public JadbConnection(String host, int port) { 22 | this.host = host; 23 | this.port = port; 24 | } 25 | 26 | public Transport createTransport() throws IOException { 27 | return new Transport(new Socket(host, port)); 28 | } 29 | 30 | public String getHostVersion() throws IOException, JadbException { 31 | try (Transport transport = createTransport()) { 32 | transport.send("host:version"); 33 | transport.verifyResponse(); 34 | return transport.readString(); 35 | } 36 | } 37 | 38 | public InetSocketAddress connectToTcpDevice(InetSocketAddress inetSocketAddress) 39 | throws IOException, JadbException, ConnectionToRemoteDeviceException { 40 | try (Transport transport = createTransport()) { 41 | return new HostConnectToRemoteTcpDevice(transport).connect(inetSocketAddress); 42 | } 43 | } 44 | 45 | public InetSocketAddress disconnectFromTcpDevice(InetSocketAddress tcpAddressEntity) 46 | throws IOException, JadbException, ConnectionToRemoteDeviceException { 47 | try (Transport transport = createTransport()) { 48 | return new HostDisconnectFromRemoteTcpDevice(transport).disconnect(tcpAddressEntity); 49 | } 50 | } 51 | 52 | public List getDevices() throws IOException, JadbException { 53 | try (Transport transport = createTransport()) { 54 | transport.send("host:devices"); 55 | transport.verifyResponse(); 56 | String body = transport.readString(); 57 | return parseDevices(body); 58 | } 59 | } 60 | 61 | public DeviceWatcher createDeviceWatcher(DeviceDetectionListener listener) throws IOException, JadbException { 62 | Transport transport = createTransport(); 63 | transport.send("host:track-devices"); 64 | transport.verifyResponse(); 65 | return new DeviceWatcher(transport, listener, this); 66 | } 67 | 68 | public List parseDevices(String body) { 69 | String[] lines = body.split("\n"); 70 | ArrayList devices = new ArrayList<>(lines.length); 71 | for (String line : lines) { 72 | String[] parts = line.split("\t"); 73 | if (parts.length > 1) { 74 | devices.add(new JadbDevice(parts[0], this)); // parts[1] is type 75 | } 76 | } 77 | return devices; 78 | } 79 | 80 | public JadbDevice getAnyDevice() { 81 | return JadbDevice.createAny(this); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/unit/PackageManagerTest.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.unit; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import se.vidstige.jadb.JadbConnection; 7 | import se.vidstige.jadb.JadbDevice; 8 | import se.vidstige.jadb.managers.Package; 9 | import se.vidstige.jadb.managers.PackageManager; 10 | import se.vidstige.jadb.test.fakes.FakeAdbServer; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | 17 | public class PackageManagerTest { 18 | private static final String DEVICE_SERIAL = "serial-123"; 19 | 20 | private FakeAdbServer server; 21 | private JadbDevice device; 22 | 23 | @Before 24 | public void setUp() throws Exception { 25 | server = new FakeAdbServer(15037); 26 | server.start(); 27 | server.add(DEVICE_SERIAL); 28 | device = new JadbConnection("localhost", 15037).getDevices().get(0); 29 | } 30 | 31 | @After 32 | public void tearDown() throws Exception { 33 | server.stop(); 34 | server.verifyExpectations(); 35 | } 36 | 37 | @Test 38 | public void testGetPackagesWithSeveralPackages() throws Exception { 39 | //Arrange 40 | List expected = new ArrayList<>(); 41 | expected.add(new Package("/system/priv-app/Contacts.apk-com.android.contacts")); 42 | expected.add(new Package("/system/priv-app/Teleservice.apk-com.android.phone")); 43 | 44 | String response = "package:/system/priv-app/Contacts.apk-com.android.contacts\n" + 45 | "package:/system/priv-app/Teleservice.apk-com.android.phone"; 46 | 47 | server.expectShell(DEVICE_SERIAL, "pm 'list' 'packages'").returns(response); 48 | 49 | //Act 50 | List actual = new PackageManager(device).getPackages(); 51 | 52 | //Assert 53 | assertEquals(expected, actual); 54 | } 55 | 56 | @Test 57 | public void testGetPackagesMalformedIgnoredString() throws Exception { 58 | //Arrange 59 | List expected = new ArrayList<>(); 60 | expected.add(new Package("/system/priv-app/Contacts.apk-com.android.contacts")); 61 | expected.add(new Package("/system/priv-app/Teleservice.apk-com.android.phone")); 62 | 63 | String response = "package:/system/priv-app/Contacts.apk-com.android.contacts\n" + 64 | "[malformed_line]\n" + 65 | "package:/system/priv-app/Teleservice.apk-com.android.phone"; 66 | 67 | server.expectShell(DEVICE_SERIAL, "pm 'list' 'packages'").returns(response); 68 | 69 | //Act 70 | List actual = new PackageManager(device).getPackages(); 71 | 72 | //Assert 73 | assertEquals(expected, actual); 74 | } 75 | 76 | @Test 77 | public void testGetPackagesWithNoPackages() throws Exception { 78 | //Arrange 79 | List expected = new ArrayList<>(); 80 | String response = ""; 81 | 82 | server.expectShell(DEVICE_SERIAL, "pm 'list' 'packages'").returns(response); 83 | 84 | //Act 85 | List actual = new PackageManager(device).getPackages(); 86 | 87 | //Assert 88 | assertEquals(expected, actual); 89 | } 90 | } -------------------------------------------------------------------------------- /test/se/vidstige/jadb/HostDisconnectFromRemoteTcpDeviceTest.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.IOException; 6 | import java.net.InetSocketAddress; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.mockito.ArgumentMatchers.anyString; 10 | import static org.mockito.Mockito.doThrow; 11 | import static org.mockito.Mockito.mock; 12 | import static org.mockito.Mockito.when; 13 | 14 | public class HostDisconnectFromRemoteTcpDeviceTest { 15 | 16 | @Test 17 | public void testNormalConnection() throws ConnectionToRemoteDeviceException, IOException, JadbException { 18 | //Prepare 19 | Transport transport = mock(Transport.class); 20 | when(transport.readString()).thenReturn("disconnected host:1"); 21 | 22 | InetSocketAddress inetSocketAddress = new InetSocketAddress("host", 1); 23 | 24 | //Do 25 | HostDisconnectFromRemoteTcpDevice hostConnectToRemoteTcpDevice = new HostDisconnectFromRemoteTcpDevice(transport); 26 | InetSocketAddress resultInetSocketAddress = hostConnectToRemoteTcpDevice.disconnect(inetSocketAddress); 27 | 28 | //Validate 29 | assertEquals(inetSocketAddress, resultInetSocketAddress); 30 | } 31 | 32 | @Test(expected = JadbException.class) 33 | public void testTransportLevelException() throws ConnectionToRemoteDeviceException, IOException, JadbException { 34 | //Prepare 35 | Transport transport = mock(Transport.class); 36 | doThrow(new JadbException("Fake exception")).when(transport).verifyResponse(); 37 | 38 | InetSocketAddress inetSocketAddress = new InetSocketAddress("host", 1); 39 | 40 | //Do 41 | HostDisconnectFromRemoteTcpDevice hostConnectToRemoteTcpDevice = new HostDisconnectFromRemoteTcpDevice(transport); 42 | hostConnectToRemoteTcpDevice.disconnect(inetSocketAddress); 43 | } 44 | 45 | @Test(expected = ConnectionToRemoteDeviceException.class) 46 | public void testProtocolException() throws ConnectionToRemoteDeviceException, IOException, JadbException { 47 | //Prepare 48 | Transport transport = mock(Transport.class); 49 | when(transport.readString()).thenReturn("any string"); 50 | HostDisconnectFromRemoteTcpDevice.ResponseValidator responseValidator = mock(HostConnectionCommand.ResponseValidator.class); 51 | doThrow(new ConnectionToRemoteDeviceException("Fake exception")).when(responseValidator).validate(anyString()); 52 | 53 | InetSocketAddress inetSocketAddress = new InetSocketAddress("host", 1); 54 | 55 | //Do 56 | HostDisconnectFromRemoteTcpDevice hostConnectToRemoteTcpDevice = new HostDisconnectFromRemoteTcpDevice(transport, responseValidator); 57 | hostConnectToRemoteTcpDevice.disconnect(inetSocketAddress); 58 | } 59 | 60 | @Test 61 | public void testProtocolResponseValidatorSuccessfullyConnected() throws ConnectionToRemoteDeviceException, IOException, JadbException { 62 | new HostDisconnectFromRemoteTcpDevice.ResponseValidatorImp().validate("disconnected 127.0.0.1:10001"); 63 | } 64 | 65 | @Test 66 | public void testProtocolResponseValidatorAlreadyConnected() throws ConnectionToRemoteDeviceException, IOException, JadbException { 67 | new HostDisconnectFromRemoteTcpDevice.ResponseValidatorImp().validate("error: no such device '127.0.0.1:10001'"); 68 | } 69 | 70 | @Test(expected = ConnectionToRemoteDeviceException.class) 71 | public void testProtocolResponseValidatorErrorInValidate() throws ConnectionToRemoteDeviceException, IOException, JadbException { 72 | new HostDisconnectFromRemoteTcpDevice.ResponseValidatorImp().validate("some error occurred"); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/ShellProcess.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.util.concurrent.*; 7 | 8 | public class ShellProcess extends Process { 9 | 10 | private static final int KILLED_STATUS_CODE = 9; 11 | private final OutputStream outputStream; 12 | private final InputStream inputStream; 13 | private final InputStream errorStream; 14 | private final Future exitCodeFuture; 15 | private final ShellProtocolTransport transport; 16 | private Integer exitCode = null; 17 | 18 | ShellProcess(OutputStream outputStream, InputStream inputStream, InputStream errorStream, Future exitCodeFuture, ShellProtocolTransport transport) { 19 | this.outputStream = outputStream; 20 | this.inputStream = inputStream; 21 | this.errorStream = errorStream; 22 | this.exitCodeFuture = exitCodeFuture; 23 | this.transport = transport; 24 | } 25 | 26 | @Override 27 | public OutputStream getOutputStream() { 28 | return outputStream; 29 | } 30 | 31 | @Override 32 | public InputStream getInputStream() { 33 | return inputStream; 34 | } 35 | 36 | @Override 37 | public InputStream getErrorStream() { 38 | return errorStream; 39 | } 40 | 41 | @Override 42 | public int waitFor() throws InterruptedException { 43 | if (exitCode == null) { 44 | try { 45 | exitCode = exitCodeFuture.get(); 46 | } catch (CancellationException e) { 47 | exitCode = KILLED_STATUS_CODE; 48 | } catch (ExecutionException e) { 49 | throw new RuntimeException(e); 50 | } 51 | } 52 | return exitCode; 53 | } 54 | 55 | /* For 1.8 */ 56 | public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException { 57 | if (exitCode == null) { 58 | try { 59 | exitCode = exitCodeFuture.get(timeout, unit); 60 | } catch (CancellationException e) { 61 | exitCode = KILLED_STATUS_CODE; 62 | } catch (ExecutionException e) { 63 | throw new RuntimeException(e); 64 | } catch (TimeoutException e) { 65 | return false; 66 | } 67 | } 68 | return true; 69 | } 70 | 71 | @Override 72 | public int exitValue() { 73 | if (exitCode != null) { 74 | return exitCode; 75 | } 76 | if (exitCodeFuture.isDone()) { 77 | try { 78 | exitCode = exitCodeFuture.get(0, TimeUnit.SECONDS); 79 | return exitCode; 80 | } catch (CancellationException e) { 81 | exitCode = KILLED_STATUS_CODE; 82 | } catch (ExecutionException e) { 83 | throw new RuntimeException(e); 84 | } catch (TimeoutException e) { 85 | // fallthrough, but should never happen 86 | } catch (InterruptedException e) { 87 | // fallthrough, but should never happen 88 | Thread.currentThread().interrupt(); 89 | } 90 | } 91 | throw new IllegalThreadStateException(); 92 | } 93 | 94 | @Override 95 | public void destroy() { 96 | if (isAlive()) { 97 | try { 98 | exitCodeFuture.cancel(true); 99 | // interrupt (usually) doesn't work for blocking read -- this will cause SocketException 100 | transport.close(); 101 | } catch (IOException e) { 102 | throw new RuntimeException(e); 103 | } 104 | } 105 | } 106 | 107 | /* For 1.8 */ 108 | public boolean isAlive() { 109 | return !exitCodeFuture.isDone(); 110 | } 111 | } -------------------------------------------------------------------------------- /test/se/vidstige/jadb/HostConnectToRemoteTcpDeviceTest.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import org.junit.Test; 4 | import org.mockito.ArgumentCaptor; 5 | 6 | import java.io.IOException; 7 | import java.net.InetSocketAddress; 8 | 9 | import static org.junit.Assert.*; 10 | import static org.mockito.ArgumentCaptor.forClass; 11 | import static org.mockito.ArgumentMatchers.anyString; 12 | import static org.mockito.Mockito.doThrow; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.times; 15 | import static org.mockito.Mockito.verify; 16 | import static org.mockito.Mockito.when; 17 | 18 | public class HostConnectToRemoteTcpDeviceTest { 19 | 20 | @Test 21 | public void testNormalConnection() throws ConnectionToRemoteDeviceException, IOException, JadbException { 22 | //Prepare 23 | Transport transport = mock(Transport.class); 24 | when(transport.readString()).thenReturn("connected to somehost:1"); 25 | 26 | InetSocketAddress inetSocketAddress = new InetSocketAddress("somehost", 1); 27 | 28 | //Do 29 | HostConnectToRemoteTcpDevice hostConnectToRemoteTcpDevice = new HostConnectToRemoteTcpDevice(transport); 30 | InetSocketAddress resultTcpAddressEntity = hostConnectToRemoteTcpDevice.connect(inetSocketAddress); 31 | 32 | //Validate 33 | assertEquals(resultTcpAddressEntity, inetSocketAddress); 34 | 35 | ArgumentCaptor argument = forClass(String.class); 36 | verify(transport, times(1)).send(argument.capture()); 37 | assertEquals("host:connect:somehost:1", argument.getValue()); 38 | } 39 | 40 | @Test(expected = JadbException.class) 41 | public void testTransportLevelException() throws ConnectionToRemoteDeviceException, IOException, JadbException { 42 | //Prepare 43 | Transport transport = mock(Transport.class); 44 | doThrow(new JadbException("Fake exception")).when(transport).verifyResponse(); 45 | 46 | InetSocketAddress tcpAddressEntity = new InetSocketAddress("somehost", 1); 47 | 48 | //Do 49 | HostConnectToRemoteTcpDevice hostConnectToRemoteTcpDevice = new HostConnectToRemoteTcpDevice(transport); 50 | hostConnectToRemoteTcpDevice.connect(tcpAddressEntity); 51 | 52 | //Validate 53 | verify(transport, times(1)).send(anyString()); 54 | verify(transport, times(1)).verifyResponse(); 55 | } 56 | 57 | @Test(expected = ConnectionToRemoteDeviceException.class) 58 | public void testProtocolException() throws ConnectionToRemoteDeviceException, IOException, JadbException { 59 | //Prepare 60 | Transport transport = mock(Transport.class); 61 | when(transport.readString()).thenReturn("connected to somehost:1"); 62 | HostConnectToRemoteTcpDevice.ResponseValidator responseValidator = mock(HostConnectionCommand.ResponseValidator.class); 63 | doThrow(new ConnectionToRemoteDeviceException("Fake exception")).when(responseValidator).validate(anyString()); 64 | 65 | InetSocketAddress tcpAddressEntity = new InetSocketAddress("somehost", 1); 66 | 67 | //Do 68 | HostConnectToRemoteTcpDevice hostConnectToRemoteTcpDevice = new HostConnectToRemoteTcpDevice(transport, responseValidator); 69 | hostConnectToRemoteTcpDevice.connect(tcpAddressEntity); 70 | 71 | //Validate 72 | verify(transport, times(1)).send(anyString()); 73 | verify(transport, times(1)).verifyResponse(); 74 | verify(responseValidator, times(1)).validate(anyString()); 75 | } 76 | 77 | @Test 78 | public void testProtocolResponseValidatorSuccessfullyConnected() throws ConnectionToRemoteDeviceException, IOException, JadbException { 79 | new HostConnectToRemoteTcpDevice.ResponseValidatorImp().validate("connected to somehost:1"); 80 | } 81 | 82 | @Test 83 | public void testProtocolResponseValidatorAlreadyConnected() throws ConnectionToRemoteDeviceException, IOException, JadbException { 84 | new HostConnectToRemoteTcpDevice.ResponseValidatorImp().validate("already connected to somehost:1"); 85 | } 86 | 87 | @Test(expected = ConnectionToRemoteDeviceException.class) 88 | public void testProtocolResponseValidatorErrorInValidate() throws ConnectionToRemoteDeviceException, IOException, JadbException { 89 | new HostConnectToRemoteTcpDevice.ResponseValidatorImp().validate("some error occurred"); 90 | } 91 | } -------------------------------------------------------------------------------- /src/se/vidstige/jadb/SyncTransport.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.*; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | /** 7 | * Created by vidstige on 2014-03-19. 8 | */ 9 | public class SyncTransport { 10 | 11 | private final DataOutput output; 12 | private final DataInput input; 13 | 14 | public SyncTransport(DataOutput outputStream, DataInput inputStream) { 15 | output = outputStream; 16 | input = inputStream; 17 | } 18 | 19 | public void send(String syncCommand, String name) throws IOException { 20 | if (syncCommand.length() != 4) throw new IllegalArgumentException("sync commands must have length 4"); 21 | output.writeBytes(syncCommand); 22 | byte[] data = name.getBytes(StandardCharsets.UTF_8); 23 | output.writeInt(Integer.reverseBytes(data.length)); 24 | output.write(data); 25 | } 26 | 27 | public void sendStatus(String statusCode, int length) throws IOException { 28 | output.writeBytes(statusCode); 29 | output.writeInt(Integer.reverseBytes(length)); 30 | } 31 | 32 | public void verifyStatus() throws IOException, JadbException { 33 | String status = readString(4); 34 | int length = readInt(); 35 | if ("FAIL".equals(status)) { 36 | String error = readString(length); 37 | throw new JadbException(error); 38 | } 39 | if (!"OKAY".equals(status)) { 40 | throw new JadbException("Unknown error: " + status); 41 | } 42 | } 43 | 44 | private int readInt() throws IOException { 45 | return Integer.reverseBytes(input.readInt()); 46 | } 47 | 48 | private String readString(int length) throws IOException { 49 | byte[] buffer = new byte[length]; 50 | input.readFully(buffer); 51 | return new String(buffer, StandardCharsets.UTF_8); 52 | } 53 | 54 | public void sendDirectoryEntry(RemoteFile file) throws IOException { 55 | output.writeBytes("DENT"); 56 | output.writeInt(Integer.reverseBytes(0666 | (file.isDirectory() ? (1 << 14) : 0))); 57 | output.writeInt(Integer.reverseBytes(file.getSize())); 58 | output.writeInt(Integer.reverseBytes(file.getLastModified())); 59 | byte[] pathChars = file.getPath().getBytes(StandardCharsets.UTF_8); 60 | output.writeInt(Integer.reverseBytes(pathChars.length)); 61 | output.write(pathChars); 62 | } 63 | 64 | public void sendDirectoryEntryDone() throws IOException { 65 | output.writeBytes("DONE"); 66 | output.writeBytes("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); // equivalent to the length of a "normal" dent 67 | } 68 | 69 | public RemoteFileRecord readDirectoryEntry() throws IOException { 70 | String id = readString(4); 71 | int mode = readInt(); 72 | int size = readInt(); 73 | int time = readInt(); 74 | int nameLength = readInt(); 75 | String name = readString(nameLength); 76 | 77 | if (!"DENT".equals(id)) return RemoteFileRecord.DONE; 78 | return new RemoteFileRecord(name, mode, size, time); 79 | } 80 | 81 | private void sendChunk(byte[] buffer, int offset, int length) throws IOException { 82 | output.writeBytes("DATA"); 83 | output.writeInt(Integer.reverseBytes(length)); 84 | output.write(buffer, offset, length); 85 | } 86 | 87 | private int readChunk(byte[] buffer) throws IOException, JadbException { 88 | String id = readString(4); 89 | int n = readInt(); 90 | if ("FAIL".equals(id)) { 91 | throw new JadbException(readString(n)); 92 | } 93 | if (!"DATA".equals(id)) return -1; 94 | input.readFully(buffer, 0, n); 95 | return n; 96 | } 97 | 98 | public void sendStream(InputStream in) throws IOException { 99 | byte[] buffer = new byte[1024 * 64]; 100 | int n = in.read(buffer); 101 | while (n != -1) { 102 | sendChunk(buffer, 0, n); 103 | n = in.read(buffer); 104 | } 105 | } 106 | 107 | public void readChunksTo(OutputStream stream) throws IOException, JadbException { 108 | byte[] buffer = new byte[1024 * 64]; 109 | int n = readChunk(buffer); 110 | while (n != -1) { 111 | stream.write(buffer, 0, n); 112 | n = readChunk(buffer); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JADB # 2 | ADB client implemented in pure Java. 3 | 4 | The Android Debug Bridge (ADB) is a client-server architecture used to communicate with Android devices (install APKs, debug apps, etc). 5 | 6 | The Android SDK Tools are available for the major platforms (Mac, Windows & Linux) and include the `adb` command line tool which implements the ADB protocol. 7 | 8 | This projects aims at providing an up to date implementation of the ADB protocol. 9 | 10 | ![Build Status](https://github.com/vidstige/jadb/actions/workflows/maven.yml/badge.svg) 11 | [![jitpack badge](https://jitpack.io/v/vidstige/jadb.svg)](https://jitpack.io/#vidstige/jadb) 12 | [![codecov](https://codecov.io/gh/vidstige/jadb/branch/master/graph/badge.svg)](https://codecov.io/gh/vidstige/jadb) 13 | [![first timers friendly](http://img.shields.io/badge/first--timers--only-friendly-green.svg?style=flat&colorB=FF69B4)](http://www.firsttimersonly.com/) 14 | 15 | 16 | ## Example ## 17 | Usage cannot be simpler. Just create a `JadbConnection` and off you go. 18 | 19 | ```java 20 | JadbConnection jadb = new JadbConnection(); 21 | List devices = jadb.getDevices(); 22 | ``` 23 | 24 | Make sure the adb server is running. You can start it by running `adb` once from the command line. 25 | 26 | It's very easy to send and receive files from your android device, for example as below. 27 | 28 | ```java 29 | JadbDevice device = ... 30 | device.pull(new RemoteFile("/path/to/file.txt"), new File("file.txt")); 31 | ``` 32 | 33 | Some high level operations such as installing and uninstalling packages are also available. 34 | 35 | ```java 36 | JadbDevice device = ... 37 | new PackageManager(device).install(new File("/path/to/my.apk")); 38 | ``` 39 | 40 | ## Protocol Description ## 41 | 42 | An overview of the protocol can be found here: [Overview](https://android.googlesource.com/platform/system/adb/+/master/OVERVIEW.TXT) 43 | 44 | A list of the available commands that a ADB Server may accept can be found here: 45 | [Services](https://android.googlesource.com/platform/system/adb/+/master/SERVICES.TXT) 46 | 47 | The description for the protocol for transferring files can be found here: [SYNC.TXT](https://android.googlesource.com/platform/system/adb/+/master/SYNC.TXT). 48 | 49 | 50 | ## Using JADB in your application ## 51 | 52 | Since version v1.1 Jadb support [maven](https://maven.apache.org/) as a build system. Although this project is not presented in official apache maven 53 | repositories this library can be used as dependencies in your maven/gradle project with the help of [jitpack](https://jitpack.io). 54 | [Jitpack](https://jitpack.io) is a system which parses github public repositories and make artifacts from them. 55 | You only need to add [jitpack](https://jitpack.io) as a repository to let maven/gradle to search for artifacts in it, like so 56 | 57 | ``` 58 | 59 | 60 | jitpack.io 61 | https://jitpack.io 62 | 63 | 64 | ``` 65 | 66 | After that you will need to add actual dependency. [Jitpack](https://jitpack.io) takes groupId, artifactId and version id from repository name, 67 | project name and tag ignoring actual values from pom.xml. So you need to write: 68 | 69 | ``` 70 | 71 | com.github.vidstige 72 | jadb 73 | v1.2.1 74 | 75 | ``` 76 | 77 | ## Troubleshooting 78 | If you cannot connect to your device check the following. 79 | 80 | - Your adb server is running by issuing `adb start-server` 81 | - You can see the device using adb `adb devices` 82 | 83 | If you see the device in `adb` but not in `jadb` please file an issue on https://github.com/vidstige/jadb/. 84 | 85 | ### Workaround for Unix Sockets Adb Server 86 | 87 | Install `socat` and issue the following to forward port 5037 to the unix domain socket. 88 | ```bash 89 | socat TCP-LISTEN:5037,reuseaddr,fork UNIX-CONNECT:/tmp/5037 90 | ``` 91 | 92 | ## Contributing ## 93 | This project would not be where it is, if it where not for the helpful [contributors](https://github.com/vidstige/jadb/graphs/contributors) 94 | supporting jadb with pull requests, issue reports, and great ideas. If _you_ would like to 95 | contribute, please read through [CONTRIBUTING.md](CONTRIBUTING.md). 96 | 97 | * If you fix a bug, try to _first_ create a failing test. Reach out to me for assistance or guidance if needed. 98 | 99 | ## Authors ## 100 | Samuel Carlsson 101 | 102 | See [contributors](https://github.com/vidstige/jadb/graphs/contributors) for a full list. 103 | 104 | ## License ## 105 | This project is released under the Apache License Version 2.0, see [LICENSE.md](LICENSE.md) for more information. 106 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/unit/PropertyManagerTest.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.unit; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import se.vidstige.jadb.JadbConnection; 7 | import se.vidstige.jadb.JadbDevice; 8 | import se.vidstige.jadb.managers.PropertyManager; 9 | import se.vidstige.jadb.test.fakes.FakeAdbServer; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | 16 | public class PropertyManagerTest { 17 | private static final String DEVICE_SERIAL = "serial-123"; 18 | 19 | private FakeAdbServer server; 20 | private JadbDevice device; 21 | 22 | @Before 23 | public void setUp() throws Exception { 24 | server = new FakeAdbServer(15037); 25 | server.start(); 26 | server.add(DEVICE_SERIAL); 27 | device = new JadbConnection("localhost", 15037).getDevices().get(0); 28 | } 29 | 30 | @After 31 | public void tearDown() throws Exception { 32 | server.stop(); 33 | server.verifyExpectations(); 34 | } 35 | 36 | @Test 37 | public void testGetPropsStandardFormat() throws Exception { 38 | //Arrange 39 | Map expected = new HashMap<>(); 40 | expected.put("bluetooth.hciattach", "true"); 41 | expected.put("bluetooth.status", "off"); 42 | 43 | String response = "[bluetooth.status]: [off] \n" + 44 | "[bluetooth.hciattach]: [true]"; 45 | 46 | server.expectShell(DEVICE_SERIAL, "getprop").returns(response); 47 | 48 | //Act 49 | Map actual = new PropertyManager(device).getprop(); 50 | 51 | //Assert 52 | assertEquals(expected, actual); 53 | } 54 | 55 | @Test 56 | public void testGetPropsValueHasSpecialCharacters() throws Exception { 57 | /* Some example properties from Nexus 9: 58 | [ro.product.model]: [Nexus 9] 59 | [ro.product.cpu.abilist]: [arm64-v8a,armeabi-v7a,armeabi] 60 | [ro.retaildemo.video_path]: [/data/preloads/demo/retail_demo.mp4] 61 | [ro.url.legal]: [http://www.google.com/intl/%s/mobile/android/basic/phone-legal.html] 62 | [ro.vendor.build.date]: [Tue Nov 1 18:21:23 UTC 2016] 63 | */ 64 | //Arrange 65 | Map expected = new HashMap<>(); 66 | expected.put("ro.product.model", "Nexus 9"); 67 | expected.put("ro.product.cpu.abilist", "arm64-v8a,armeabi-v7a,armeabi"); 68 | expected.put("ro.retaildemo.video_path", "/data/preloads/demo/retail_demo.mp4"); 69 | expected.put("ro.url.legal", "http://www.google.com/intl/%s/mobile/android/basic/phone-legal.html"); 70 | expected.put("ro.vendor.build.date", "Tue Nov 1 18:21:23 UTC 2016"); 71 | 72 | String response = "[ro.product.model]: [Nexus 9]\n" + 73 | "[ro.product.cpu.abilist]: [arm64-v8a,armeabi-v7a,armeabi]\n" + 74 | "[ro.retaildemo.video_path]: [/data/preloads/demo/retail_demo.mp4]\n" + 75 | "[ro.url.legal]: [http://www.google.com/intl/%s/mobile/android/basic/phone-legal.html]\n" + 76 | "[ro.vendor.build.date]: [Tue Nov 1 18:21:23 UTC 2016]"; 77 | 78 | server.expectShell(DEVICE_SERIAL, "getprop").returns(response); 79 | 80 | //Act 81 | Map actual = new PropertyManager(device).getprop(); 82 | 83 | //Assert 84 | assertEquals(expected, actual); 85 | } 86 | 87 | @Test 88 | public void testGetPropsMalformedIgnoredString() throws Exception { 89 | //Arrange 90 | Map expected = new HashMap<>(); 91 | expected.put("bluetooth.hciattach", "true"); 92 | expected.put("bluetooth.status", "off"); 93 | 94 | String response = "[bluetooth.status]: [off]\n" + 95 | "[malformed_line]\n" + 96 | "[bluetooth.hciattach]: [true]"; 97 | 98 | server.expectShell(DEVICE_SERIAL, "getprop").returns(response); 99 | 100 | //Act 101 | Map actual = new PropertyManager(device).getprop(); 102 | 103 | //Assert 104 | assertEquals(expected, actual); 105 | } 106 | 107 | @Test 108 | public void testGetPropsMalformedNotUsedString() throws Exception { 109 | //Arrange 110 | Map expected = new HashMap<>(); 111 | expected.put("bluetooth.status", "off"); 112 | 113 | String response = "[bluetooth.status]: [off]\n" + 114 | "malformed[bluetooth.hciattach]: [true]"; 115 | 116 | server.expectShell(DEVICE_SERIAL, "getprop").returns(response); 117 | 118 | //Act 119 | Map actual = new PropertyManager(device).getprop(); 120 | 121 | //Assert 122 | assertEquals(expected, actual); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/ShellProtocolTransport.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.*; 4 | 5 | class ShellProtocolTransport implements Closeable { 6 | private final DataOutputStream output; 7 | private final DataInputStream input; 8 | 9 | ShellProtocolTransport(DataOutputStream outputStream, DataInputStream inputStream) { 10 | output = outputStream; 11 | input = inputStream; 12 | } 13 | 14 | // replace with Integer.toUnsignedLong in Java 8 15 | private static long integerToUnsignedLong(int i) { 16 | return ((long) (int) i) & 0xffffffffL; 17 | } 18 | 19 | // replace with Byte.toUnsignedInt in Java 8 20 | private static int byteToUnsignedInt(byte b) { 21 | return ((int) b) & 0xff; 22 | } 23 | 24 | private ShellMessageType readMessageType() throws IOException { 25 | return ShellMessageType.fromId(input.readByte()); 26 | } 27 | 28 | private long readDataLength() throws IOException { 29 | return integerToUnsignedLong(Integer.reverseBytes(input.readInt())); 30 | } 31 | 32 | private void readDataTo(OutputStream out, long dataLength, byte[] buffer) throws IOException { 33 | long remaining = dataLength; 34 | while (remaining > 0) { 35 | int len = (int) Math.min(remaining, buffer.length); 36 | input.readFully(buffer, 0, len); 37 | out.write(buffer, 0, len); 38 | remaining -= len; 39 | } 40 | out.flush(); 41 | } 42 | 43 | int demuxOutput(OutputStream outputStream, OutputStream errorStream) throws JadbException, IOException { 44 | int exitCode = 0; 45 | byte[] buf = new byte[256 * 1024]; 46 | 47 | try { 48 | while (true) { 49 | ShellMessageType messageType = readMessageType(); 50 | long length = readDataLength(); 51 | switch (messageType) { 52 | case STDOUT: 53 | readDataTo(outputStream, length, buf); 54 | break; 55 | case STDERR: 56 | readDataTo(errorStream, length, buf); 57 | break; 58 | case EXIT: 59 | if (length != 1) { 60 | throw new JadbException("Expected only one byte for exitCode"); 61 | } 62 | exitCode = byteToUnsignedInt(input.readByte()); 63 | break; 64 | default: 65 | // ignore; 66 | break; 67 | } 68 | } 69 | } catch (EOFException e) { 70 | // ignore 71 | } 72 | 73 | return exitCode; 74 | } 75 | 76 | private void writeData(ShellMessageType type, byte[] buf, int off, int len) throws IOException { 77 | output.writeByte(byteToUnsignedInt(type.getId())); 78 | output.writeInt(Integer.reverseBytes(len)); 79 | output.write(buf, off, len); 80 | } 81 | 82 | OutputStream getOutputStream() { 83 | return new ShellProtocolOutputStream(this); 84 | } 85 | 86 | @Override 87 | public void close() throws IOException { 88 | output.close(); 89 | input.close(); 90 | } 91 | 92 | enum ShellMessageType { 93 | STDIN((byte) 0), STDOUT((byte) 1), STDERR((byte) 2), EXIT((byte) 3), CLOSE_STDIN((byte) 4), WINDOW_SIZE_CHANGE((byte) 5), UNKNOWN(Byte.MIN_VALUE); 94 | 95 | private final byte id; 96 | 97 | ShellMessageType(byte id) { 98 | this.id = id; 99 | } 100 | 101 | public static ShellMessageType fromId(byte b) { 102 | switch (b) { 103 | case 0: 104 | return STDIN; 105 | case 1: 106 | return STDOUT; 107 | case 2: 108 | return STDERR; 109 | case 3: 110 | return EXIT; 111 | case 4: 112 | return CLOSE_STDIN; 113 | case 5: 114 | // unused 115 | return WINDOW_SIZE_CHANGE; 116 | default: 117 | return UNKNOWN; 118 | } 119 | } 120 | 121 | public byte getId() { 122 | return id; 123 | } 124 | } 125 | 126 | private static class ShellProtocolOutputStream extends OutputStream { 127 | 128 | private final ShellProtocolTransport transport; 129 | 130 | ShellProtocolOutputStream(ShellProtocolTransport transport) { 131 | this.transport = transport; 132 | } 133 | 134 | @Override 135 | public void write(int b) throws IOException { 136 | write(new byte[]{(byte) b}); 137 | } 138 | 139 | @Override 140 | public void write(byte[] b, int off, int len) throws IOException { 141 | transport.writeData(ShellMessageType.STDIN, b, off, len); 142 | } 143 | 144 | @Override 145 | public void flush() throws IOException { 146 | transport.output.flush(); 147 | } 148 | 149 | @Override 150 | public void close() throws IOException { 151 | transport.writeData(ShellMessageType.CLOSE_STDIN, new byte[0], 0, 0); 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /src/se/vidstige/jadb/managers/PackageManager.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.managers; 2 | 3 | import se.vidstige.jadb.JadbDevice; 4 | import se.vidstige.jadb.JadbException; 5 | import se.vidstige.jadb.RemoteFile; 6 | import se.vidstige.jadb.Stream; 7 | 8 | import java.io.*; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | /** 15 | * Java interface to package manager. Launches package manager through jadb 16 | */ 17 | public class PackageManager { 18 | private final JadbDevice device; 19 | 20 | public PackageManager(JadbDevice device) { 21 | this.device = device; 22 | } 23 | 24 | public List getPackages() throws IOException, JadbException { 25 | try (BufferedReader input = new BufferedReader(new InputStreamReader(device.executeShell("pm", "list", "packages"), StandardCharsets.UTF_8))) { 26 | ArrayList result = new ArrayList<>(); 27 | String line; 28 | while ((line = input.readLine()) != null) { 29 | final String prefix = "package:"; 30 | if (line.startsWith(prefix)) { 31 | result.add(new Package(line.substring(prefix.length()))); 32 | } 33 | } 34 | return result; 35 | } 36 | } 37 | 38 | private String getErrorMessage(String operation, String target, String errorMessage) { 39 | return "Could not " + operation + " " + target + ": " + errorMessage; 40 | } 41 | 42 | private void verifyOperation(String operation, String target, String result) throws JadbException { 43 | if (!result.contains("Success")) throw new JadbException(getErrorMessage(operation, target, result)); 44 | } 45 | 46 | private void remove(RemoteFile file) throws IOException, JadbException { 47 | InputStream s = device.executeShell("rm", "-f", file.getPath()); 48 | Stream.readAll(s, StandardCharsets.UTF_8); 49 | } 50 | 51 | private void install(File apkFile, List extraArguments) throws IOException, JadbException { 52 | RemoteFile remote = new RemoteFile("/data/local/tmp/" + apkFile.getName()); 53 | device.push(apkFile, remote); 54 | List arguments = new ArrayList<>(); 55 | arguments.add("install"); 56 | arguments.addAll(extraArguments); 57 | arguments.add(remote.getPath()); 58 | InputStream s = device.executeShell("pm", arguments.toArray(new String[0])); 59 | String result = Stream.readAll(s, StandardCharsets.UTF_8); 60 | remove(remote); 61 | verifyOperation("install", apkFile.getName(), result); 62 | } 63 | 64 | public void install(File apkFile) throws IOException, JadbException { 65 | install(apkFile, new ArrayList(0)); 66 | } 67 | 68 | public void installWithOptions(File apkFile, List options) throws IOException, JadbException { 69 | List optionsAsStr = new ArrayList<>(options.size()); 70 | 71 | for(InstallOption installOption : options) { 72 | optionsAsStr.add(installOption.getStringRepresentation()); 73 | } 74 | install(apkFile, optionsAsStr); 75 | } 76 | 77 | public void forceInstall(File apkFile) throws IOException, JadbException { 78 | installWithOptions(apkFile, Collections.singletonList(REINSTALL_KEEPING_DATA)); 79 | } 80 | 81 | public void uninstall(Package name) throws IOException, JadbException { 82 | InputStream s = device.executeShell("pm", "uninstall", name.toString()); 83 | String result = Stream.readAll(s, StandardCharsets.UTF_8); 84 | verifyOperation("uninstall", name.toString(), result); 85 | } 86 | 87 | public void launch(Package name) throws IOException, JadbException { 88 | InputStream s = device.executeShell("monkey", "-p", name.toString(), "-c", "android.intent.category.LAUNCHER", "1"); 89 | s.close(); 90 | } 91 | 92 | // 93 | public static class InstallOption { 94 | private final StringBuilder stringBuilder = new StringBuilder(); 95 | 96 | InstallOption(String ... varargs) { 97 | String suffix = ""; 98 | for(String str: varargs) { 99 | stringBuilder.append(suffix).append(str); 100 | suffix = " "; 101 | } 102 | } 103 | 104 | private String getStringRepresentation() { 105 | return stringBuilder.toString(); 106 | } 107 | } 108 | 109 | public static final InstallOption WITH_FORWARD_LOCK = new InstallOption("-l"); 110 | 111 | public static final InstallOption REINSTALL_KEEPING_DATA = 112 | new InstallOption("-r"); 113 | 114 | public static final InstallOption ALLOW_TEST_APK = 115 | new InstallOption("-t"); 116 | 117 | @SuppressWarnings("squid:S00100") 118 | public static InstallOption WITH_INSTALLER_PACKAGE_NAME(String name) 119 | { 120 | return new InstallOption("-t", name); 121 | } 122 | 123 | @SuppressWarnings("squid:S00100") 124 | public static InstallOption ON_SHARED_MASS_STORAGE(String name) { 125 | return new InstallOption("-s", name); 126 | } 127 | 128 | @SuppressWarnings("squid:S00100") 129 | public static InstallOption ON_INTERNAL_SYSTEM_MEMORY(String name) { 130 | return new InstallOption("-f", name); 131 | } 132 | 133 | public static final InstallOption ALLOW_VERSION_DOWNGRADE = 134 | new InstallOption("-d"); 135 | 136 | /** 137 | * This option is supported only from Android 6.X+ 138 | */ 139 | public static final InstallOption GRANT_ALL_PERMISSIONS = new InstallOption("-g"); 140 | 141 | /** 142 | * This option is supported only from Android 14.X+ 143 | */ 144 | public static final InstallOption UPDATE_OWNERSHIP = new InstallOption("--update-ownership"); 145 | // 146 | } 147 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/integration/RealDeviceTestCases.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.integration; 2 | 3 | import org.junit.*; 4 | import org.junit.rules.TemporaryFolder; 5 | import se.vidstige.jadb.*; 6 | 7 | import java.io.*; 8 | import java.net.InetSocketAddress; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.List; 11 | import java.util.Scanner; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.atomic.AtomicReference; 16 | 17 | import static org.junit.Assert.*; 18 | 19 | public class RealDeviceTestCases { 20 | 21 | @Rule 22 | public TemporaryFolder temporaryFolder = new TemporaryFolder(); //Must be public 23 | private JadbConnection jadb; 24 | 25 | @BeforeClass 26 | public static void tryToStartAdbServer() { 27 | try { 28 | new AdbServerLauncher(new Subprocess(), System.getenv()).launch(); 29 | } catch (IOException | InterruptedException e) { 30 | System.out.println("Could not start adb-server"); 31 | } 32 | } 33 | 34 | @Before 35 | public void connect() throws IOException { 36 | try { 37 | jadb = new JadbConnection(); 38 | jadb.getHostVersion(); 39 | } catch (Exception e) { 40 | org.junit.Assume.assumeNoException(e); 41 | } 42 | } 43 | 44 | @Test 45 | public void testGetHostVersion() throws Exception { 46 | jadb.getHostVersion(); 47 | } 48 | 49 | @Test 50 | public void testGetDevices() throws Exception { 51 | List actual = jadb.getDevices(); 52 | Assert.assertNotNull(actual); 53 | //Assert.assertEquals("emulator-5554", actual.get(0).getSerial()); 54 | } 55 | 56 | @Test 57 | public void testListFilesTwice() throws Exception { 58 | JadbDevice any = jadb.getAnyDevice(); 59 | for (RemoteFile f : any.list("/")) { 60 | System.out.println(f.getPath()); 61 | } 62 | 63 | for (RemoteFile f : any.list("/")) { 64 | System.out.println(f.getPath()); 65 | } 66 | } 67 | 68 | @Test 69 | public void testPushFile() throws Exception { 70 | JadbDevice any = jadb.getAnyDevice(); 71 | any.push(new File("README.md"), new RemoteFile("/sdcard/README.md")); 72 | //second read on the same device 73 | any.push(new File("README.md"), new RemoteFile("/sdcard/README.md")); 74 | } 75 | 76 | @Test(expected = JadbException.class) 77 | public void testPushFileToInvalidPath() throws Exception { 78 | JadbDevice any = jadb.getAnyDevice(); 79 | any.push(new File("README.md"), new RemoteFile("/no/such/directory/README.md")); 80 | } 81 | 82 | @Test 83 | public void testPullFile() throws Exception { 84 | JadbDevice any = jadb.getAnyDevice(); 85 | any.pull(new RemoteFile("/sdcard/README.md"), temporaryFolder.newFile("foobar.md")); 86 | //second read on the same device 87 | any.pull(new RemoteFile("/sdcard/README.md"), temporaryFolder.newFile("foobar.md")); 88 | } 89 | 90 | @Test(expected = JadbException.class) 91 | public void testPullInvalidFile() throws Exception { 92 | JadbDevice any = jadb.getAnyDevice(); 93 | any.pull(new RemoteFile("/file/does/not/exist"), temporaryFolder.newFile("xyz")); 94 | } 95 | 96 | @SuppressWarnings("deprecation") 97 | @Test 98 | public void testShellExecuteTwice() throws Exception { 99 | JadbDevice any = jadb.getAnyDevice(); 100 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); 101 | any.executeShell(bout, "ls /"); 102 | any.executeShell(bout, "ls", "-la", "/"); 103 | byte[] buf = bout.toByteArray(); 104 | System.out.write(buf, 0, buf.length); 105 | } 106 | 107 | @Test 108 | public void testShellProcessBuilderStart() throws Exception { 109 | JadbDevice any = jadb.getAnyDevice(); 110 | Process process = any.shellProcessBuilder("ls /").start(); 111 | AtomicReference stdout = new AtomicReference<>(); 112 | AtomicReference stderr = new AtomicReference<>(); 113 | Thread thread1 = gobbler(process.getInputStream(), stdout); 114 | Thread thread2 = gobbler(process.getErrorStream(), stderr); 115 | thread1.start(); 116 | thread2.start(); 117 | process.waitFor(); 118 | thread1.join(); 119 | thread2.join(); 120 | System.out.println(stdout.get()); 121 | System.out.println(stderr.get()); 122 | } 123 | 124 | private Thread gobbler(final InputStream stream, final AtomicReference out) { 125 | return new Thread(new Runnable() { 126 | @Override 127 | public void run() { 128 | out.set(new Scanner(stream).useDelimiter("\\A").next()); 129 | } 130 | }); 131 | } 132 | 133 | @Test 134 | public void testShellExecuteProcessRedirectToOutputStream() throws Exception { 135 | JadbDevice any = jadb.getAnyDevice(); 136 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 137 | ByteArrayOutputStream err = new ByteArrayOutputStream(); 138 | Process process = any.shellProcessBuilder("ls /") 139 | .redirectOutput(out) 140 | .redirectError(err) 141 | .start(); 142 | process.waitFor(); 143 | System.out.println(out.toString(StandardCharsets.UTF_8.name())); 144 | System.out.println(err.toString(StandardCharsets.UTF_8.name())); 145 | } 146 | 147 | @Test 148 | public void testShellExecuteProcessRedirectErrorStream() throws Exception { 149 | JadbDevice any = jadb.getAnyDevice(); 150 | Process process = any.shellProcessBuilder("ls /").redirectErrorStream(true).start(); 151 | String stdout = new Scanner(process.getInputStream()).useDelimiter("\\A").next(); 152 | process.waitFor(); 153 | System.out.println(stdout); 154 | } 155 | 156 | @Test 157 | public void testShellExecuteProcessDestroy() throws Exception { 158 | JadbDevice anyDevice = jadb.getAnyDevice(); 159 | ExecutorService executor = Executors.newSingleThreadExecutor(); 160 | ShellProcess process = anyDevice.shellProcessBuilder("sleep 30").redirectErrorStream(true).useExecutor(executor).start(); 161 | process.destroy(); 162 | assertEquals(process.waitFor(), 9); 163 | executor.shutdown(); 164 | assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); 165 | } 166 | 167 | @Test 168 | public void testScreenshot() throws Exception { 169 | JadbDevice any = jadb.getAnyDevice(); 170 | try (FileOutputStream outputStream = new FileOutputStream(temporaryFolder.newFile("screenshot.png"))) { 171 | InputStream stdout = any.executeShell("screencap", "-p"); 172 | Stream.copy(stdout, outputStream); 173 | } 174 | } 175 | 176 | /** 177 | * This test requires emulator running on non-standard tcp port - this may be achieve by executing such command: 178 | * ${ANDROID_HOME}/emulator -verbose -avd ${NAME} -ports 10000,10001 179 | * 180 | * @throws IOException 181 | * @throws JadbException 182 | * @throws ConnectionToRemoteDeviceException 183 | */ 184 | @Test 185 | public void testConnectionToTcpDevice() throws IOException, JadbException, ConnectionToRemoteDeviceException { 186 | jadb.connectToTcpDevice(new InetSocketAddress("127.0.0.1", 10001)); 187 | List devices = jadb.getDevices(); 188 | 189 | assertNotNull(devices); 190 | assertFalse(devices.isEmpty()); 191 | } 192 | 193 | /** 194 | * @throws IOException 195 | * @throws JadbException 196 | * @throws ConnectionToRemoteDeviceException 197 | * @see #testConnectionToTcpDevice() 198 | */ 199 | @Test 200 | public void testDisconnectionToTcpDevice() throws IOException, JadbException, ConnectionToRemoteDeviceException { 201 | testConnectionToTcpDevice(); 202 | 203 | jadb.disconnectFromTcpDevice(new InetSocketAddress("127.0.0.1", 10001)); 204 | jadb.getDevices(); 205 | 206 | List devices = jadb.getDevices(); 207 | assertNotNull(devices); 208 | assertTrue(devices.isEmpty()); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | app.revanced 8 | jadb 9 | 1.2.1.1 10 | https://github.com/vidstige/jadb 11 | 12 | 13 | 14 | The Apache License, Version 2.0 15 | http://www.apache.org/licenses/LICENSE-2.0.txt 16 | 17 | 18 | 19 | 20 | 21 | vidstige 22 | Samuel Carlsson 23 | samuel.carlsson@gmai.com 24 | https://github.com/vidstige 25 | 26 | 27 | 28 | 29 | UTF-8 30 | UTF-8 31 | 1.7 32 | 1.7 33 | 2.28.2 34 | 35 | 36 | 37 | 38 | junit 39 | junit 40 | 4.13.1 41 | test 42 | 43 | 44 | 45 | 46 | org.mockito 47 | mockito-core 48 | ${mockito-core.version} 49 | 50 | 51 | 52 | 53 | src 54 | test 55 | 56 | 70 | 71 | org.codehaus.mojo 72 | versions-maven-plugin 73 | 2.3 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-compiler-plugin 79 | 3.5.1 80 | 81 | 1.7 82 | 1.7 83 | 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-source-plugin 88 | 3.0.1 89 | 90 | 91 | verify 92 | 93 | jar-no-fork 94 | 95 | 96 | 97 | 98 | 99 | org.apache.maven.plugins 100 | maven-release-plugin 101 | 2.5.3 102 | 103 | false 104 | release 105 | true 106 | deploy 107 | 108 | 109 | 110 | org.apache.maven.scm 111 | maven-scm-provider-gitexe 112 | 1.8.1 113 | 114 | 115 | 116 | 117 | org.jacoco 118 | jacoco-maven-plugin 119 | 0.8.4 120 | 121 | 122 | 123 | prepare-agent 124 | 125 | 126 | 127 | report 128 | test 129 | 130 | report 131 | 132 | 133 | 134 | 135 | 136 | org.apache.maven.plugins 137 | maven-surefire-plugin 138 | 2.19.1 139 | 140 | 141 | **/*.java 142 | 143 | 144 | 145 | se.vidstige.jadb.test.integration.* 146 | 147 | **/data/* 148 | **/fakes/* 149 | 150 | 151 | 152 | 153 | org.apache.maven.plugins 154 | maven-failsafe-plugin 155 | 2.19.1 156 | 157 | 158 | **/*.java 159 | 160 | 161 | 162 | se.vidstige.jadb.test.integration.* 163 | 164 | **/data/* 165 | **/fakes/* 166 | 167 | 168 | 169 | 170 | 171 | integration-test 172 | verify 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | release 183 | 184 | 185 | 186 | maven-source-plugin 187 | 188 | 189 | attach-sources 190 | 191 | jar 192 | 193 | 194 | 195 | 196 | 197 | org.apache.maven.plugins 198 | maven-javadoc-plugin 199 | 2.10.4 200 | 201 | 202 | attach-javadocs 203 | 204 | jar 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | scm:git:git@github.com:vidstige/jadb.git 216 | scm:git:git@github.com:vidstige/jadb.git 217 | scm:git:git@github.com:vidstige/jadb.git 218 | HEAD 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/server/AdbProtocolHandler.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.server; 2 | 3 | import se.vidstige.jadb.JadbException; 4 | import se.vidstige.jadb.RemoteFile; 5 | import se.vidstige.jadb.SyncTransport; 6 | 7 | import java.io.*; 8 | import java.net.ProtocolException; 9 | import java.net.Socket; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | class AdbProtocolHandler implements Runnable { 13 | private final Socket socket; 14 | private final AdbResponder responder; 15 | private AdbDeviceResponder selected; 16 | 17 | public AdbProtocolHandler(Socket socket, AdbResponder responder) { 18 | this.socket = socket; 19 | this.responder = responder; 20 | } 21 | 22 | private AdbDeviceResponder findDevice(String serial) throws ProtocolException { 23 | for (AdbDeviceResponder d : responder.getDevices()) { 24 | if (d.getSerial().equals(serial)) return d; 25 | } 26 | throw new ProtocolException("'" + serial + "' not connected"); 27 | } 28 | 29 | @Override 30 | public void run() { 31 | try { 32 | runServer(); 33 | } catch (IOException e) { 34 | if (e.getMessage() != null) // thrown when exiting for some reason 35 | System.out.println("IO Error: " + e.getMessage()); 36 | } 37 | } 38 | 39 | private void runServer() throws IOException { 40 | try ( 41 | DataInputStream input = new DataInputStream(socket.getInputStream()); 42 | DataOutputStream output = new DataOutputStream(socket.getOutputStream()) 43 | ) { 44 | //noinspection StatementWithEmptyBody 45 | while (processCommand(input, output)) { 46 | // nothing to do here 47 | } 48 | } 49 | } 50 | 51 | private boolean processCommand(DataInput input, DataOutputStream output) throws IOException { 52 | String command = readCommand(input); 53 | responder.onCommand(command); 54 | 55 | try { 56 | if ("host:version".equals(command)) { 57 | hostVersion(output); 58 | } else if ("host:transport-any".equals(command)) { 59 | hostTransportAny(output); 60 | } else if ("host:devices".equals(command)) { 61 | hostDevices(output); 62 | } else if (command.startsWith("host:transport:")) { 63 | hostTransport(output, command); 64 | } else if ("sync:".equals(command)) { 65 | sync(output, input); 66 | } else if (command.startsWith("shell")) { 67 | shell(input, output, command); 68 | return false; 69 | } else if ("host:get-state".equals(command)) { 70 | hostGetState(output); 71 | } else if (command.startsWith("host-serial:")) { 72 | hostSerial(output, command); 73 | } else if (command.startsWith("tcpip:")) { 74 | handleTcpip(output, command); 75 | } else { 76 | throw new ProtocolException("Unknown command: " + command); 77 | } 78 | } catch (ProtocolException e) { 79 | output.writeBytes("FAIL"); 80 | send(output, e.getMessage()); 81 | } 82 | output.flush(); 83 | return true; 84 | } 85 | 86 | private void handleTcpip(DataOutputStream output, String command) throws IOException { 87 | output.writeBytes("OKAY"); 88 | selected.enableIpCommand(command.substring("tcpip:".length()), output); 89 | } 90 | 91 | private void hostSerial(DataOutput output, String command) throws IOException { 92 | String[] strs = command.split(":",0); 93 | if (strs.length != 3) { 94 | throw new ProtocolException("Invalid command: " + command); 95 | } 96 | 97 | String serial = strs[1]; 98 | boolean found = false; 99 | output.writeBytes("OKAY"); 100 | for (AdbDeviceResponder d : responder.getDevices()) { 101 | if (d.getSerial().equals(serial)) { 102 | send(output, d.getType()); 103 | found = true; 104 | break; 105 | } 106 | } 107 | 108 | if (!found) { 109 | send(output, "unknown"); 110 | } 111 | } 112 | 113 | private void hostGetState(DataOutput output) throws IOException { 114 | // TODO: Check so that exactly one device is selected. 115 | AdbDeviceResponder device = responder.getDevices().get(0); 116 | output.writeBytes("OKAY"); 117 | send(output, device.getType()); 118 | } 119 | 120 | private void shell(DataInput input, DataOutputStream output, String command) throws IOException { 121 | String shellCommand = command.split(":", 2)[1]; 122 | output.writeBytes("OKAY"); 123 | shell(shellCommand, output, input); 124 | } 125 | 126 | private void hostTransport(DataOutput output, String command) throws IOException { 127 | String serial = command.substring("host:transport:".length()); 128 | selected = findDevice(serial); 129 | output.writeBytes("OKAY"); 130 | } 131 | 132 | private void hostDevices(DataOutput output) throws IOException { 133 | ByteArrayOutputStream tmp = new ByteArrayOutputStream(); 134 | DataOutputStream writer = new DataOutputStream(tmp); 135 | for (AdbDeviceResponder d : responder.getDevices()) { 136 | writer.writeBytes(d.getSerial() + "\t" + d.getType() + "\n"); 137 | } 138 | output.writeBytes("OKAY"); 139 | send(output, new String(tmp.toByteArray(), StandardCharsets.UTF_8)); 140 | } 141 | 142 | private void hostTransportAny(DataOutput output) throws IOException { 143 | // TODO: Check so that exactly one device is selected. 144 | selected = responder.getDevices().get(0); 145 | output.writeBytes("OKAY"); 146 | } 147 | 148 | private void hostVersion(DataOutput output) throws IOException { 149 | output.writeBytes("OKAY"); 150 | send(output, String.format("%04x", responder.getVersion())); 151 | } 152 | 153 | private void shell(String command, DataOutputStream stdout, DataInput stdin) throws IOException { 154 | selected.shell(command, stdout, stdin); 155 | } 156 | 157 | private int readInt(DataInput input) throws IOException { 158 | return Integer.reverseBytes(input.readInt()); 159 | } 160 | 161 | private int readHexInt(DataInput input) throws IOException { 162 | return Integer.parseInt(readString(input, 4), 16); 163 | } 164 | 165 | private String readString(DataInput input, int length) throws IOException { 166 | byte[] responseBuffer = new byte[length]; 167 | input.readFully(responseBuffer); 168 | return new String(responseBuffer, StandardCharsets.UTF_8); 169 | } 170 | 171 | private String readCommand(DataInput input) throws IOException { 172 | int length = readHexInt(input); 173 | return readString(input, length); 174 | } 175 | 176 | private void sync(DataOutput output, DataInput input) throws IOException { 177 | output.writeBytes("OKAY"); 178 | try { 179 | String id = readString(input, 4); 180 | int length = readInt(input); 181 | switch (id) { 182 | case "SEND": 183 | syncSend(output, input, length); 184 | break; 185 | case "RECV": 186 | syncRecv(output, input, length); 187 | break; 188 | case "LIST": 189 | syncList(output, input, length); 190 | break; 191 | default: 192 | throw new JadbException("Unknown sync id " + id); 193 | } 194 | } catch (JadbException e) { // sync response with a different type of fail message 195 | SyncTransport sync = getSyncTransport(output, input); 196 | sync.send("FAIL", e.getMessage()); 197 | } 198 | } 199 | 200 | private void syncRecv(DataOutput output, DataInput input, int length) throws IOException, JadbException { 201 | String remotePath = readString(input, length); 202 | SyncTransport transport = getSyncTransport(output, input); 203 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 204 | selected.filePulled(new RemoteFile(remotePath), buffer); 205 | transport.sendStream(new ByteArrayInputStream(buffer.toByteArray())); 206 | transport.sendStatus("DONE", 0); // ignored 207 | } 208 | 209 | private void syncSend(DataOutput output, DataInput input, int length) throws IOException, JadbException { 210 | String remotePath = readString(input, length); 211 | int idx = remotePath.lastIndexOf(','); 212 | String path = remotePath; 213 | int mode = 0666; 214 | if (idx > 0) { 215 | path = remotePath.substring(0, idx); 216 | mode = Integer.parseInt(remotePath.substring(idx + 1)); 217 | } 218 | SyncTransport transport = getSyncTransport(output, input); 219 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 220 | transport.readChunksTo(buffer); 221 | selected.filePushed(new RemoteFile(path), mode, buffer); 222 | transport.sendStatus("OKAY", 0); // 0 = ignored 223 | } 224 | 225 | private void syncList(DataOutput output, DataInput input, int length) throws IOException, JadbException { 226 | String remotePath = readString(input, length); 227 | SyncTransport transport = getSyncTransport(output, input); 228 | for (RemoteFile file : selected.list(remotePath)) { 229 | transport.sendDirectoryEntry(file); 230 | } 231 | transport.sendDirectoryEntryDone(); 232 | } 233 | 234 | private String getCommandLength(String command) { 235 | return String.format("%04x", command.length()); 236 | } 237 | 238 | private void send(DataOutput writer, String response) throws IOException { 239 | writer.writeBytes(getCommandLength(response)); 240 | writer.writeBytes(response); 241 | } 242 | 243 | private SyncTransport getSyncTransport(DataOutput output, DataInput input) { 244 | return new SyncTransport(output, input); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/ShellProcessBuilder.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import java.io.*; 4 | import java.nio.file.Files; 5 | import java.nio.file.StandardOpenOption; 6 | import java.util.concurrent.*; 7 | 8 | /** 9 | * A builder of a {@link Process} corresponding to an ADB shell command. 10 | * 11 | *

This builder allows for configuration of the {@link Process}'s output and error streams as well as the 12 | * {@link Executor} to use when starting the shell process. The output and error streams may be either be redirected 13 | * (using {@link java.lang.ProcessBuilder.Redirect}) or given an explicit {@link OutputStream}. You may also combine 14 | * the output and error streams via {@link #redirectErrorStream(boolean) redirectErrorStream(true)}.

15 | * 16 | *

Use {@link #start()} to execute the command, and then use {@link Process#waitFor()} to wait for the command to 17 | * complete.

18 | * 19 | *

Warning: If stdout and stderr are both set to {@link java.lang.ProcessBuilder.Redirect#PIPE} (the default), 20 | * you must read from their InputStreams ({@link Process#getInputStream()} and {@link Process#getErrorStream()}) 21 | * concurrently. This requires having two separate threads to read the input streams separately. Otherwise, 22 | * the process may deadlock. To avoid using threads, you can use {@link #redirectErrorStream(boolean)}, in which case 23 | * you must read all output from {@link Process#getInputStream()} before calling {@link Process#waitFor()}: 24 | * 25 | *

{@code
 26 |  *   Process process = jadbDevice.shellProcessBuilder("command")
 27 |  *       .redirectErrorStream(errorStream)
 28 |  *       .start();
 29 |  *   String stdoutAndStderr = new Scanner(process.getInputStream()).useDelimiter("\\A").next();
 30 |  *   int exitCode = process.waitFor();
 31 |  * }
32 | *

33 | * You can also use one of the {@code redirectOutput} methods to have the output automatically redirected. For example, 34 | * to buffer all of stdout and stderr separately, you can use {@link java.io.ByteArrayOutputStream}: 35 | * 36 | *

{@code
 37 |  *   ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 38 |  *   ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
 39 |  *   Process process = jadbDevice.shellProcessBuilder("command")
 40 |  *       .redirectOutput(outputStream)
 41 |  *       .redirectError(errorStream)
 42 |  *       .start();
 43 |  *   int exitCode = process.waitFor();
 44 |  *   String stdout = outputStream.toString(StandardCharsets.UTF_8.name());
 45 |  *   String stderr = errorStream.toString(StandardCharsets.UTF_8.name());
 46 |  * }
47 | */ 48 | public class ShellProcessBuilder { 49 | 50 | private JadbDevice device; 51 | private String command; 52 | private ProcessBuilder.Redirect outRedirect = ProcessBuilder.Redirect.PIPE; 53 | private OutputStream outOs = null; 54 | private ProcessBuilder.Redirect errRedirect = ProcessBuilder.Redirect.PIPE; 55 | private OutputStream errOs = null; 56 | private boolean redirectErrorStream; 57 | private Executor executor = null; 58 | 59 | ShellProcessBuilder(JadbDevice device, String command) { 60 | this.device = device; 61 | this.command = command; 62 | } 63 | 64 | private void checkValidForWrite(ProcessBuilder.Redirect destination) { 65 | if (destination.type() == ProcessBuilder.Redirect.Type.READ) { 66 | throw new IllegalArgumentException("Redirect invalid for writing: " + destination); 67 | } 68 | } 69 | 70 | /** 71 | * Redirect stdout to the given destination. If set to anything other than 72 | * {@link java.lang.ProcessBuilder.Redirect#PIPE} (the default), {@link Process#getInputStream()} does nothing. 73 | * 74 | * @param destination where to redirect 75 | * @return this 76 | */ 77 | public ShellProcessBuilder redirectOutput(ProcessBuilder.Redirect destination) { 78 | checkValidForWrite(destination); 79 | outRedirect = destination; 80 | outOs = null; 81 | return this; 82 | } 83 | 84 | /** 85 | * Redirect stdout directly to the given output stream. 86 | *

Note: this output steam will be called from a separate thread.

87 | * 88 | * @param destination OutputStream to write 89 | * @return this 90 | */ 91 | public ShellProcessBuilder redirectOutput(OutputStream destination) { 92 | outRedirect = null; 93 | outOs = destination; 94 | return this; 95 | } 96 | 97 | /** 98 | * Redirect stderr to the given destination. If set to anything other than 99 | * {@link java.lang.ProcessBuilder.Redirect#PIPE} (the default), {@link Process#getErrorStream()} does nothing. 100 | * 101 | * @param destination where to redirect 102 | * @return this 103 | */ 104 | public ShellProcessBuilder redirectError(ProcessBuilder.Redirect destination) { 105 | checkValidForWrite(destination); 106 | errRedirect = destination; 107 | errOs = null; 108 | return this; 109 | } 110 | 111 | /** 112 | * Redirect stderr directly to the given output stream. 113 | *

Note: this output steam will be called from a separate thread.

114 | * 115 | * @param destination OutputStream to write 116 | * @return this 117 | */ 118 | public ShellProcessBuilder redirectError(OutputStream destination) { 119 | errRedirect = null; 120 | errOs = destination; 121 | return this; 122 | } 123 | 124 | /** 125 | * Set redirecting of the error stream directly to the output stream. If set, any {@code redirectError} calls are 126 | * ignored, and the returned Process 127 | * 128 | * @param redirectErrorStream true to enable redirecting of the error stream 129 | * @return this 130 | */ 131 | public ShellProcessBuilder redirectErrorStream(boolean redirectErrorStream) { 132 | this.redirectErrorStream = redirectErrorStream; 133 | return this; 134 | } 135 | 136 | /** 137 | * Set the {@link Executor} to use to run the process handling thread. If not set, uses 138 | * {@link Executors#newSingleThreadExecutor()}. 139 | * 140 | * @param executor An executor 141 | * @return this 142 | */ 143 | public ShellProcessBuilder useExecutor(Executor executor) { 144 | this.executor = executor; 145 | return this; 146 | } 147 | 148 | /** 149 | * Starts the shell command. 150 | * 151 | * @return a {@link Process} 152 | * @throws IOException 153 | * @throws JadbException 154 | */ 155 | public ShellProcess start() throws IOException, JadbException { 156 | Transport transport = null; 157 | try { 158 | final OutputStream outOs = getOutputStream(this.outOs, this.outRedirect, System.out); 159 | InputStream outIs = getConnectedPipe(outOs); 160 | 161 | final OutputStream errOs; 162 | InputStream errIs; 163 | if (redirectErrorStream) { 164 | errOs = outOs; 165 | errIs = NullInputStream.INSTANCE; 166 | } else { 167 | errOs = getOutputStream(this.errOs, this.errRedirect, System.err); 168 | errIs = getConnectedPipe(errOs); 169 | } 170 | 171 | transport = device.getTransport(); 172 | final ShellProtocolTransport shellProtocolTransport = transport.startShellProtocol(this.command); 173 | OutputStream inOs = shellProtocolTransport.getOutputStream(); 174 | 175 | FutureTask transportTask = new FutureTask<>(new Callable() { 176 | @Override 177 | public Integer call() throws Exception { 178 | try (ShellProtocolTransport unused1 = shellProtocolTransport; OutputStream unused2 = outOs; OutputStream unused3 = errOs) { 179 | return shellProtocolTransport.demuxOutput(outOs, errOs); 180 | } 181 | } 182 | }); 183 | 184 | if (executor == null) { 185 | ExecutorService service = Executors.newSingleThreadExecutor(); 186 | service.execute(transportTask); 187 | service.shutdown(); 188 | } else { 189 | executor.execute(transportTask); 190 | } 191 | 192 | return new ShellProcess(inOs, outIs, errIs, transportTask, shellProtocolTransport); 193 | } catch (IOException | JadbException | RuntimeException e) { 194 | if (transport != null) { 195 | transport.close(); 196 | } 197 | throw e; 198 | } 199 | } 200 | 201 | private OutputStream getOutputStream(OutputStream os, ProcessBuilder.Redirect destination, OutputStream inherit) throws IOException { 202 | if (os != null) { 203 | return os; 204 | } 205 | switch (destination.type()) { 206 | case PIPE: 207 | return new PipedOutputStream(); 208 | case INHERIT: 209 | return inherit; 210 | case READ: 211 | throw new IllegalArgumentException("Redirect invalid for writing: " + destination); 212 | case WRITE: 213 | return Files.newOutputStream(destination.file().toPath()); 214 | case APPEND: 215 | return Files.newOutputStream(destination.file().toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE); 216 | default: 217 | throw new IllegalArgumentException("Unknown redirect type: " + destination); 218 | } 219 | } 220 | 221 | private InputStream getConnectedPipe(OutputStream os) throws IOException { 222 | if (os instanceof PipedOutputStream) { 223 | return new PipedInputStream((PipedOutputStream) os); 224 | } 225 | return NullInputStream.INSTANCE; 226 | } 227 | 228 | static class NullInputStream extends InputStream { 229 | static final NullInputStream INSTANCE = new NullInputStream(); 230 | 231 | private NullInputStream() { 232 | } 233 | 234 | public int read() { 235 | return -1; 236 | } 237 | 238 | public int available() { 239 | return 0; 240 | } 241 | } 242 | } -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/unit/MockedTestCases.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.unit; 2 | 3 | import org.junit.After; 4 | import org.junit.Assert; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import se.vidstige.jadb.JadbConnection; 8 | import se.vidstige.jadb.JadbDevice; 9 | import se.vidstige.jadb.JadbException; 10 | import se.vidstige.jadb.RemoteFile; 11 | import se.vidstige.jadb.test.fakes.FakeAdbServer; 12 | 13 | import java.io.ByteArrayInputStream; 14 | import java.io.ByteArrayOutputStream; 15 | import java.io.DataOutputStream; 16 | import java.io.IOException; 17 | import java.nio.charset.StandardCharsets; 18 | import java.text.DateFormat; 19 | import java.text.ParseException; 20 | import java.text.SimpleDateFormat; 21 | import java.util.List; 22 | import java.util.Scanner; 23 | 24 | public class MockedTestCases { 25 | 26 | private FakeAdbServer server; 27 | private JadbConnection connection; 28 | 29 | private static long parseDate(String date) throws ParseException { 30 | DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); 31 | return dateFormat.parse(date).getTime(); 32 | } 33 | 34 | private static void assertHasFile(String expPath, int expSize, long expModifyTime, List actualFiles) { 35 | for (RemoteFile file : actualFiles) { 36 | if (expPath.equals(file.getPath())) { 37 | if (file.isDirectory()) { 38 | Assert.fail("File " + expPath + " was listed as a dir!"); 39 | } else if (expSize != file.getSize() || expModifyTime != file.getLastModified()) { 40 | Assert.fail("File " + expPath + " exists but has incorrect properties!"); 41 | } else { 42 | return; 43 | } 44 | } 45 | } 46 | Assert.fail("File " + expPath + " could not be found!"); 47 | } 48 | 49 | private static void assertHasDir(String expPath, long expModifyTime, List actualFiles) { 50 | for (RemoteFile file : actualFiles) { 51 | if (expPath.equals(file.getPath())) { 52 | if (!file.isDirectory()) { 53 | Assert.fail("Dir " + expPath + " was listed as a file!"); 54 | } else if (expModifyTime != file.getLastModified()) { 55 | Assert.fail("Dir " + expPath + " exists but has incorrect properties!"); 56 | } else { 57 | return; 58 | } 59 | } 60 | } 61 | Assert.fail("Dir " + expPath + " could not be found!"); 62 | } 63 | 64 | @Before 65 | public void setUp() throws Exception { 66 | server = new FakeAdbServer(15037); 67 | server.start(); 68 | connection = new JadbConnection("localhost", 15037); 69 | } 70 | 71 | @After 72 | public void tearDown() throws Exception { 73 | server.stop(); 74 | server.verifyExpectations(); 75 | } 76 | 77 | @Test 78 | public void testGetHostVersion() throws Exception { 79 | Assert.assertEquals("001f", connection.getHostVersion()); 80 | } 81 | 82 | @Test 83 | public void testListDevices() throws Exception { 84 | server.add("serial-123"); 85 | List devices = connection.getDevices(); 86 | Assert.assertEquals("serial-123", devices.get(0).getSerial()); 87 | } 88 | 89 | @Test 90 | public void testGetDeviceState() throws Exception { 91 | server.add("serial-1", "offline"); 92 | server.add("serial-2", "device"); 93 | server.add("serial-3", "unknown"); 94 | server.add("serial-4", "foobar"); 95 | List devices = connection.getDevices(); 96 | Assert.assertEquals(JadbDevice.State.Offline, devices.get(0).getState()); 97 | Assert.assertEquals(JadbDevice.State.Device, devices.get(1).getState()); 98 | Assert.assertEquals(JadbDevice.State.Unknown, devices.get(2).getState()); 99 | Assert.assertEquals(JadbDevice.State.Unknown, devices.get(3).getState()); 100 | } 101 | 102 | @Test 103 | public void testListNoDevices() throws Exception { 104 | List devices = connection.getDevices(); 105 | Assert.assertEquals(0, devices.size()); 106 | } 107 | 108 | @Test 109 | public void testPushFile() throws Exception { 110 | server.add("serial-123"); 111 | server.expectPush("serial-123", new RemoteFile("/remote/path/abc.txt")).withContent("abc"); 112 | JadbDevice device = connection.getDevices().get(0); 113 | ByteArrayInputStream fileContents = new ByteArrayInputStream("abc".getBytes(StandardCharsets.UTF_8)); 114 | device.push(fileContents, parseDate("1981-08-25 13:37"), 0666, new RemoteFile("/remote/path/abc.txt")); 115 | } 116 | 117 | @Test(expected = JadbException.class) 118 | public void testPushToInvalidPath() throws Exception { 119 | server.add("serial-123"); 120 | server.expectPush("serial-123", new RemoteFile("/remote/path/abc.txt")).failWith("No such directory"); 121 | JadbDevice device = connection.getDevices().get(0); 122 | ByteArrayInputStream fileContents = new ByteArrayInputStream("abc".getBytes(StandardCharsets.UTF_8)); 123 | device.push(fileContents, parseDate("1981-08-25 13:37"), 0666, new RemoteFile("/remote/path/abc.txt")); 124 | } 125 | 126 | @Test 127 | public void testPullFile() throws Exception { 128 | server.add("serial-123"); 129 | server.expectPull("serial-123", new RemoteFile("/remote/path/abc.txt")).withContent("foobar"); 130 | JadbDevice device = connection.getDevices().get(0); 131 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 132 | device.pull(new RemoteFile("/remote/path/abc.txt"), buffer); 133 | Assert.assertArrayEquals("foobar".getBytes(StandardCharsets.UTF_8), buffer.toByteArray()); 134 | } 135 | 136 | @Test 137 | public void testExecuteShell() throws Exception { 138 | server.add("serial-123"); 139 | server.expectShell("serial-123", "ls '-l'").returns("total 0"); 140 | JadbDevice device = connection.getDevices().get(0); 141 | device.executeShell("ls", "-l"); 142 | } 143 | 144 | @Test 145 | public void testExecuteEnableTcpip() throws IOException, JadbException { 146 | server.add("serial-123"); 147 | server.expectTcpip("serial-123", 5555); 148 | JadbDevice device = connection.getDevices().get(0); 149 | device.enableAdbOverTCP(); 150 | } 151 | 152 | @Test 153 | public void testExecuteShellQuotesWhitespace() throws Exception { 154 | server.add("serial-123"); 155 | server.expectShell("serial-123", "ls 'space file'").returns("space file"); 156 | server.expectShell("serial-123", "echo 'tab\tstring'").returns("tab\tstring"); 157 | server.expectShell("serial-123", "echo 'newline1\nstring'").returns("newline1\nstring"); 158 | server.expectShell("serial-123", "echo 'newline2\r\nstring'").returns("newline2\r\nstring"); 159 | server.expectShell("serial-123", "echo 'fuö äzpo'").returns("fuö äzpo"); 160 | server.expectShell("serial-123", "echo 'h¡t]&poli'").returns("h¡t]&poli"); 161 | JadbDevice device = connection.getDevices().get(0); 162 | device.executeShell("ls", "space file"); 163 | device.executeShell("echo", "tab\tstring"); 164 | device.executeShell("echo", "newline1\nstring"); 165 | device.executeShell("echo", "newline2\r\nstring"); 166 | device.executeShell("echo", "fuö äzpo"); 167 | device.executeShell("echo", "h¡t]&poli"); 168 | } 169 | 170 | @Test 171 | public void testExecuteShellProtocol() throws Exception { 172 | server.add("serial-123"); 173 | server.expectShell("serial-123", "ls -l").returns(buildStream(null, null, 0)); 174 | server.expectShell("serial-123", "ls foobar").returns(buildStream("123", "456", 0)); 175 | JadbDevice device = connection.getDevices().get(0); 176 | 177 | Assert.assertEquals(device.shellProcessBuilder("ls", "-l").start().waitFor(), 0); 178 | 179 | Process process = device.shellProcessBuilder("ls", "foobar").redirectErrorStream(true).start(); 180 | Assert.assertEquals(new Scanner(process.getInputStream()).useDelimiter("\\A").next(), "123456"); 181 | Assert.assertEquals(process.waitFor(), 0); 182 | } 183 | 184 | private String buildStream(String out, String err, int exitCode) throws Exception { 185 | ByteArrayOutputStream os = new ByteArrayOutputStream(); 186 | DataOutputStream dos = new DataOutputStream(os); 187 | if (out != null) { 188 | os.write(1); 189 | dos.writeInt(Integer.reverseBytes(out.length())); 190 | os.write(out.getBytes(StandardCharsets.US_ASCII)); 191 | } 192 | if (err != null) { 193 | os.write(2); 194 | dos.writeInt(Integer.reverseBytes(err.length())); 195 | os.write(err.getBytes(StandardCharsets.US_ASCII)); 196 | } 197 | os.write(3); // exitcode stream 198 | dos.writeInt(Integer.reverseBytes(1)); 199 | os.write(exitCode); 200 | return os.toString(StandardCharsets.US_ASCII.name()); 201 | } 202 | 203 | @Test 204 | public void testFileList() throws Exception { 205 | server.add("serial-123"); 206 | server.expectList("serial-123", "/sdcard/Documents") 207 | .withDir("school", 123456789) 208 | .withDir("finances", 7070707) 209 | .withDir("\u904A\u6232", 528491) 210 | .withFile("user_manual.pdf", 3000, 648649) 211 | .withFile("effective java vol. 7.epub", 0xCAFE, 0xBABE) 212 | .withFile("\uB9AC\uADF8 \uC624\uBE0C \uB808\uC804\uB4DC", 240, 9001); 213 | JadbDevice device = connection.getDevices().get(0); 214 | List files = device.list("/sdcard/Documents"); 215 | Assert.assertEquals(6, files.size()); 216 | assertHasDir("school", 123456789, files); 217 | assertHasDir("finances", 7070707, files); 218 | assertHasDir("\u904A\u6232", 528491, files); 219 | assertHasFile("user_manual.pdf", 3000, 648649, files); 220 | assertHasFile("effective java vol. 7.epub", 0xCAFE, 0xBABE, files); 221 | assertHasFile("\uB9AC\uADF8 \uC624\uBE0C \uB808\uC804\uB4DC", 240, 9001, files); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/se/vidstige/jadb/JadbDevice.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb; 2 | 3 | import se.vidstige.jadb.managers.Bash; 4 | 5 | import java.io.*; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | public class JadbDevice { 11 | @SuppressWarnings("squid:S00115") 12 | public enum State { 13 | Unknown, 14 | Offline, 15 | Device, 16 | Recovery, 17 | BootLoader, 18 | Unauthorized, 19 | Authorizing, 20 | Sideload, 21 | Connecting, 22 | Rescue 23 | } 24 | 25 | //noinspection OctalInteger 26 | private static final int DEFAULT_MODE = 0664; 27 | private final String serial; 28 | private final ITransportFactory transportFactory; 29 | private static final int DEFAULT_TCPIP_PORT = 5555; 30 | 31 | JadbDevice(String serial, ITransportFactory tFactory) { 32 | this.serial = serial; 33 | this.transportFactory = tFactory; 34 | } 35 | 36 | static JadbDevice createAny(JadbConnection connection) { 37 | return new JadbDevice(connection); 38 | } 39 | 40 | private JadbDevice(ITransportFactory tFactory) { 41 | serial = null; 42 | this.transportFactory = tFactory; 43 | } 44 | 45 | private State convertState(String type) { 46 | switch (type) { 47 | case "device": return State.Device; 48 | case "offline": return State.Offline; 49 | case "bootloader": return State.BootLoader; 50 | case "recovery": return State.Recovery; 51 | case "unauthorized": return State.Unauthorized; 52 | case "authorizing" : return State.Authorizing; 53 | case "connecting": return State.Connecting; 54 | case "sideload": return State.Sideload; 55 | case "rescue" : return State.Rescue; 56 | default: return State.Unknown; 57 | } 58 | } 59 | 60 | Transport getTransport() throws IOException, JadbException { 61 | Transport transport = transportFactory.createTransport(); 62 | // Do not use try-with-resources here. We want to return unclosed Transport and it is up to caller 63 | // to close it. Here we close it only in case of exception. 64 | try { 65 | send(transport, serial == null ? "host:transport-any" : "host:transport:" + serial ); 66 | } catch (IOException|JadbException e) { 67 | transport.close(); 68 | throw e; 69 | } 70 | return transport; 71 | } 72 | 73 | public String getSerial() { 74 | return serial; 75 | } 76 | 77 | public State getState() throws IOException, JadbException { 78 | try (Transport transport = transportFactory.createTransport()) { 79 | send(transport, serial == null ? "host:get-state" : "host-serial:" + serial + ":get-state"); 80 | return convertState(transport.readString()); 81 | } 82 | } 83 | 84 | /**

Execute a shell command.

85 | * 86 | *

For Lollipop and later see: {@link #execute(String, String...)}

87 | * 88 | * @param command main command to run. E.g. "ls" 89 | * @param args arguments to the command. 90 | * @return combined stdout/stderr stream. 91 | * @throws IOException 92 | * @throws JadbException 93 | */ 94 | public InputStream executeShell(String command, String... args) throws IOException, JadbException { 95 | Transport transport = getTransport(); 96 | StringBuilder shellLine = buildCmdLine(command, args); 97 | send(transport, "shell:" + shellLine.toString()); 98 | return new AdbFilterInputStream(new BufferedInputStream(transport.getInputStream())); 99 | } 100 | 101 | /** 102 | * 103 | * @deprecated Use InputStream executeShell(String command, String... args) method instead. Together with 104 | * Stream.copy(in, out), it is possible to achieve the same effect. 105 | */ 106 | @Deprecated 107 | public void executeShell(OutputStream output, String command, String... args) throws IOException, JadbException { 108 | try (Transport transport = getTransport()) { 109 | StringBuilder shellLine = buildCmdLine(command, args); 110 | send(transport, "shell:" + shellLine.toString()); 111 | if (output == null) 112 | return; 113 | 114 | AdbFilterOutputStream out = new AdbFilterOutputStream(output); 115 | transport.readResponseTo(out); 116 | } 117 | } 118 | 119 | /**

Execute a shell command.

120 | * 121 | *

This method supports separate stdin, stdout, and stderr streams, as well as a return code. The shell command 122 | * is not executed until calling {@link ShellProcessBuilder#start()}, which returns a {@link Process}.

123 | * 124 | * @param command main command to run, e.g. "screencap" 125 | * @param args arguments to the command, e.g. "-p". 126 | * @return a {@link ShellProcessBuilder} 127 | */ 128 | public ShellProcessBuilder shellProcessBuilder(String command, String... args) { 129 | return new ShellProcessBuilder(this, buildCmdLine(command, args).toString()); 130 | } 131 | 132 | /**

Execute a command with raw binary output.

133 | * 134 | *

Support for this command was added in Lollipop (Android 5.0), and is the recommended way to transmit binary 135 | * data with that version or later. For earlier versions of Android, use 136 | * {@link #executeShell(String, String...)}.

137 | * 138 | * @param command main command to run, e.g. "screencap" 139 | * @param args arguments to the command, e.g. "-p". 140 | * @return combined stdout/stderr stream. 141 | * @throws IOException 142 | * @throws JadbException 143 | */ 144 | public InputStream execute(String command, String... args) throws IOException, JadbException { 145 | Transport transport = getTransport(); 146 | StringBuilder shellLine = buildCmdLine(command, args); 147 | send(transport, "exec:" + shellLine.toString()); 148 | return new BufferedInputStream(transport.getInputStream()); 149 | } 150 | 151 | /** 152 | * Builds a command line string from the command and its arguments. 153 | * 154 | * @param command the command. 155 | * @param args the list of arguments. 156 | * @return the command line. 157 | */ 158 | private StringBuilder buildCmdLine(String command, String... args) { 159 | StringBuilder shellLine = new StringBuilder(command); 160 | for (String arg : args) { 161 | shellLine.append(" "); 162 | shellLine.append(Bash.quote(arg)); 163 | } 164 | return shellLine; 165 | } 166 | 167 | /** 168 | * Enable tcpip on the default port (5555) 169 | * 170 | * @return success or failure 171 | */ 172 | public void enableAdbOverTCP() throws IOException, JadbException { 173 | enableAdbOverTCP(DEFAULT_TCPIP_PORT); 174 | } 175 | 176 | /** 177 | * Enable tcpip on a specific port 178 | * 179 | * @param port for the device to bind on 180 | * 181 | * @return success or failure 182 | */ 183 | public void enableAdbOverTCP(int port) throws IOException, JadbException { 184 | try (Transport transport = getTransport()) { 185 | send(transport, String.format("tcpip:%d", port)); 186 | } 187 | } 188 | 189 | public List list(String remotePath) throws IOException, JadbException { 190 | try (Transport transport = getTransport()) { 191 | SyncTransport sync = transport.startSync(); 192 | sync.send("LIST", remotePath); 193 | 194 | List result = new ArrayList<>(); 195 | for (RemoteFileRecord dent = sync.readDirectoryEntry(); dent != RemoteFileRecord.DONE; dent = sync.readDirectoryEntry()) { 196 | result.add(dent); 197 | } 198 | return result; 199 | } 200 | } 201 | 202 | public void push(InputStream source, long lastModified, int mode, RemoteFile remote) throws IOException, JadbException { 203 | try (Transport transport = getTransport()) { 204 | SyncTransport sync = transport.startSync(); 205 | sync.send("SEND", remote.getPath() + "," + mode); 206 | 207 | sync.sendStream(source); 208 | 209 | sync.sendStatus("DONE", (int) lastModified); 210 | sync.verifyStatus(); 211 | } 212 | } 213 | 214 | public void push(File local, RemoteFile remote) throws IOException, JadbException { 215 | try (FileInputStream fileStream = new FileInputStream(local)) { 216 | push(fileStream, TimeUnit.MILLISECONDS.toSeconds(local.lastModified()), DEFAULT_MODE, remote); 217 | } 218 | } 219 | 220 | public void pull(RemoteFile remote, OutputStream destination) throws IOException, JadbException { 221 | try (Transport transport = getTransport()) { 222 | SyncTransport sync = transport.startSync(); 223 | sync.send("RECV", remote.getPath()); 224 | 225 | sync.readChunksTo(destination); 226 | } 227 | } 228 | 229 | public void pull(RemoteFile remote, File local) throws IOException, JadbException { 230 | try (FileOutputStream fileStream = new FileOutputStream(local)) { 231 | pull(remote, fileStream); 232 | } 233 | } 234 | 235 | private void send(Transport transport, String command) throws IOException, JadbException { 236 | transport.send(command); 237 | transport.verifyResponse(); 238 | } 239 | 240 | @Override 241 | public String toString() { 242 | return "Android Device with serial " + serial; 243 | } 244 | 245 | @Override 246 | public int hashCode() { 247 | final int prime = 31; 248 | int result = 1; 249 | result = prime * result + ((serial == null) ? 0 : serial.hashCode()); 250 | return result; 251 | } 252 | 253 | @Override 254 | public boolean equals(Object obj) { 255 | if (this == obj) 256 | return true; 257 | if (obj == null) 258 | return false; 259 | if (getClass() != obj.getClass()) 260 | return false; 261 | JadbDevice other = (JadbDevice) obj; 262 | if (serial == null) { 263 | return other.serial == null; 264 | } 265 | return serial.equals(other.serial); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /test/se/vidstige/jadb/test/fakes/FakeAdbServer.java: -------------------------------------------------------------------------------- 1 | package se.vidstige.jadb.test.fakes; 2 | 3 | import se.vidstige.jadb.JadbException; 4 | import se.vidstige.jadb.RemoteFile; 5 | import se.vidstige.jadb.server.AdbDeviceResponder; 6 | import se.vidstige.jadb.server.AdbResponder; 7 | import se.vidstige.jadb.server.AdbServer; 8 | 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.DataInput; 11 | import java.io.DataOutputStream; 12 | import java.io.IOException; 13 | import java.net.ProtocolException; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.ArrayList; 16 | import java.util.Collections; 17 | import java.util.List; 18 | 19 | /** 20 | * Created by vidstige on 2014-03-20. 21 | */ 22 | public class FakeAdbServer implements AdbResponder { 23 | private final AdbServer server; 24 | private final List devices = new ArrayList<>(); 25 | 26 | public FakeAdbServer(int port) { 27 | server = new AdbServer(this, port); 28 | } 29 | 30 | public void start() throws InterruptedException { 31 | System.out.println("Starting fake on port " + server.getPort()); 32 | server.start(); 33 | } 34 | 35 | public void stop() throws IOException, InterruptedException { 36 | System.out.println("Stopping fake on port " + server.getPort()); 37 | server.stop(); 38 | } 39 | 40 | @Override 41 | public void onCommand(String command) { 42 | System.out.println("command: " + command); 43 | } 44 | 45 | @Override 46 | public int getVersion() { 47 | return 31; 48 | } 49 | 50 | public void add(String serial) { 51 | devices.add(new DeviceResponder(serial, "device")); 52 | } 53 | 54 | public void add(String serial, String type) { 55 | devices.add(new DeviceResponder(serial, type)); 56 | } 57 | 58 | public void verifyExpectations() { 59 | for (DeviceResponder d : devices) 60 | d.verifyExpectations(); 61 | } 62 | 63 | public interface ExpectationBuilder { 64 | void failWith(String message); 65 | 66 | void withContent(byte[] content); 67 | 68 | void withContent(String content); 69 | } 70 | 71 | private DeviceResponder findBySerial(String serial) { 72 | for (DeviceResponder d : devices) { 73 | if (d.getSerial().equals(serial)) return d; 74 | } 75 | return null; 76 | } 77 | 78 | public ExpectationBuilder expectPush(String serial, RemoteFile path) { 79 | return findBySerial(serial).expectPush(path); 80 | } 81 | 82 | public ExpectationBuilder expectPull(String serial, RemoteFile path) { 83 | return findBySerial(serial).expectPull(path); 84 | } 85 | 86 | public DeviceResponder.ShellExpectation expectShell(String serial, String commands) { 87 | return findBySerial(serial).expectShell(commands); 88 | } 89 | 90 | public void expectTcpip(String serial, Integer port) { 91 | findBySerial(serial).expectTcpip(port); 92 | } 93 | 94 | public DeviceResponder.ListExpectation expectList(String serial, String remotePath) { 95 | return findBySerial(serial).expectList(remotePath); 96 | } 97 | 98 | @Override 99 | public List getDevices() { 100 | return new ArrayList(devices); 101 | } 102 | 103 | private static class DeviceResponder implements AdbDeviceResponder { 104 | private final String serial; 105 | private final String type; 106 | private List fileExpectations = new ArrayList<>(); 107 | private List shellExpectations = new ArrayList<>(); 108 | private List listExpectations = new ArrayList<>(); 109 | private List tcpipExpectations = new ArrayList<>(); 110 | 111 | private DeviceResponder(String serial, String type) { 112 | this.serial = serial; 113 | this.type = type; 114 | } 115 | 116 | @Override 117 | public String getSerial() { 118 | return serial; 119 | } 120 | 121 | @Override 122 | public String getType() { 123 | return type; 124 | } 125 | 126 | @Override 127 | public void filePushed(RemoteFile path, int mode, ByteArrayOutputStream buffer) throws JadbException { 128 | for (FileExpectation fe : fileExpectations) { 129 | if (fe.matches(path)) { 130 | fileExpectations.remove(fe); 131 | fe.throwIfFail(); 132 | fe.verifyContent(buffer.toByteArray()); 133 | return; 134 | } 135 | } 136 | throw new JadbException("Unexpected push to device " + serial + " at " + path); 137 | } 138 | 139 | @Override 140 | public void filePulled(RemoteFile path, ByteArrayOutputStream buffer) throws JadbException, IOException { 141 | for (FileExpectation fe : fileExpectations) { 142 | if (fe.matches(path)) { 143 | fileExpectations.remove(fe); 144 | fe.throwIfFail(); 145 | fe.returnFile(buffer); 146 | return; 147 | } 148 | } 149 | throw new JadbException("Unexpected push to device " + serial + " at " + path); 150 | } 151 | 152 | @Override 153 | public void shell(String command, DataOutputStream stdout, DataInput stdin) throws IOException { 154 | for (ShellExpectation se : shellExpectations) { 155 | if (se.matches(command)) { 156 | shellExpectations.remove(se); 157 | se.writeOutputTo(stdout); 158 | return; 159 | } 160 | } 161 | throw new ProtocolException("Unexpected shell to device " + serial + ": " + command); 162 | } 163 | 164 | @Override 165 | public void enableIpCommand(String port, DataOutputStream outputStream) throws IOException { 166 | for (Integer expectation : tcpipExpectations) { 167 | if (expectation == Integer.parseInt(port)) { 168 | tcpipExpectations.remove(expectation); 169 | return; 170 | } 171 | } 172 | 173 | throw new ProtocolException("Unexpected tcpip to device " + serial + ": (port) " + port); 174 | 175 | } 176 | 177 | @Override 178 | public List list(String path) throws IOException { 179 | for (ListExpectation le : listExpectations) { 180 | if (le.matches(path)) { 181 | listExpectations.remove(le); 182 | return le.getFiles(); 183 | } 184 | } 185 | throw new ProtocolException("Unexpected list of device " + serial + " in dir " + path); 186 | } 187 | 188 | public void verifyExpectations() { 189 | for (FileExpectation expectation : fileExpectations) { 190 | org.junit.Assert.fail(expectation.toString()); 191 | } 192 | for (ShellExpectation expectation : shellExpectations) { 193 | org.junit.Assert.fail(expectation.toString()); 194 | } 195 | for (ListExpectation expectation : listExpectations) { 196 | org.junit.Assert.fail(expectation.toString()); 197 | } 198 | for (int expectation : tcpipExpectations) { 199 | org.junit.Assert.fail("Expected tcp/ip on" + expectation); 200 | } 201 | } 202 | 203 | private static class FileExpectation implements ExpectationBuilder { 204 | private final RemoteFile path; 205 | private byte[] content; 206 | private String failMessage; 207 | 208 | public FileExpectation(RemoteFile path) { 209 | this.path = path; 210 | content = null; 211 | failMessage = null; 212 | } 213 | 214 | @Override 215 | public void failWith(String message) { 216 | failMessage = message; 217 | } 218 | 219 | @Override 220 | public void withContent(byte[] content) { 221 | this.content = content; 222 | } 223 | 224 | @Override 225 | public void withContent(String content) { 226 | this.content = content.getBytes(StandardCharsets.UTF_8); 227 | } 228 | 229 | public boolean matches(RemoteFile path) { 230 | return this.path.equals(path); 231 | } 232 | 233 | public void throwIfFail() throws JadbException { 234 | if (failMessage != null) throw new JadbException(failMessage); 235 | } 236 | 237 | public void verifyContent(byte[] content) { 238 | org.junit.Assert.assertArrayEquals(this.content, content); 239 | } 240 | 241 | public void returnFile(ByteArrayOutputStream buffer) throws IOException { 242 | buffer.write(content); 243 | } 244 | 245 | @Override 246 | public String toString() { 247 | return "Expected file " + path; 248 | } 249 | } 250 | 251 | public static class ShellExpectation { 252 | private final String command; 253 | private byte[] stdout; 254 | 255 | public ShellExpectation(String command) { 256 | this.command = command; 257 | } 258 | 259 | public boolean matches(String command) { 260 | return command.equals(this.command); 261 | } 262 | 263 | public void returns(String stdout) { 264 | this.stdout = stdout.getBytes(StandardCharsets.UTF_8); 265 | } 266 | 267 | public void writeOutputTo(DataOutputStream stdout) throws IOException { 268 | stdout.write(this.stdout); 269 | } 270 | 271 | @Override 272 | public String toString() { 273 | return "Expected shell " + command; 274 | } 275 | } 276 | 277 | public static class ListExpectation { 278 | 279 | private final String remotePath; 280 | private final List files = new ArrayList<>(); 281 | 282 | public ListExpectation(String remotePath) { 283 | this.remotePath = remotePath; 284 | } 285 | 286 | public boolean matches(String remotePath) { 287 | return remotePath.equals(this.remotePath); 288 | } 289 | 290 | public ListExpectation withFile(String path, int size, int modifyTime) { 291 | files.add(new MockFileEntry(path, size, modifyTime, false)); 292 | return this; 293 | } 294 | 295 | public ListExpectation withDir(String path, int modifyTime) { 296 | files.add(new MockFileEntry(path, -1, modifyTime, true)); 297 | return this; 298 | } 299 | 300 | public List getFiles() { 301 | return Collections.unmodifiableList(files); 302 | } 303 | 304 | @Override 305 | public String toString() { 306 | return "Expected file list " + remotePath; 307 | } 308 | 309 | private static class MockFileEntry extends RemoteFile { 310 | 311 | private final int size; 312 | private final int modifyTime; 313 | private final boolean dir; 314 | 315 | MockFileEntry(String path, int size, int modifyTime, boolean dir) { 316 | super(path); 317 | this.size = size; 318 | this.modifyTime = modifyTime; 319 | this.dir = dir; 320 | } 321 | 322 | public int getSize() { 323 | return size; 324 | } 325 | 326 | public int getLastModified() { 327 | return modifyTime; 328 | } 329 | 330 | public boolean isDirectory() { 331 | return dir; 332 | } 333 | 334 | } 335 | 336 | } 337 | 338 | public ExpectationBuilder expectPush(RemoteFile path) { 339 | FileExpectation expectation = new FileExpectation(path); 340 | fileExpectations.add(expectation); 341 | return expectation; 342 | } 343 | 344 | public ExpectationBuilder expectPull(RemoteFile path) { 345 | FileExpectation expectation = new FileExpectation(path); 346 | fileExpectations.add(expectation); 347 | return expectation; 348 | } 349 | 350 | public ShellExpectation expectShell(String command) { 351 | ShellExpectation expectation = new ShellExpectation(command); 352 | shellExpectations.add(expectation); 353 | return expectation; 354 | } 355 | 356 | public ListExpectation expectList(String remotePath) { 357 | ListExpectation expectation = new ListExpectation(remotePath); 358 | listExpectations.add(expectation); 359 | return expectation; 360 | } 361 | 362 | public void expectTcpip(int port) { 363 | tcpipExpectations.add(port); 364 | } 365 | } 366 | } 367 | --------------------------------------------------------------------------------