diff --git a/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java b/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java index a21cb66..37b331c 100644 --- a/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java +++ b/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java @@ -17,6 +17,7 @@ import java.io.UnsupportedEncodingException; import java.net.ConnectException; import java.net.Socket; +import java.net.SocketTimeoutException; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.interfaces.RSAPublicKey; @@ -35,11 +36,8 @@ public class AdbConnection implements Closeable { public static final String TAG = AdbConnection.class.getSimpleName(); - /** - * The underlying socket that this class uses to communicate with the target device. - */ @NonNull - private final Socket mSocket; + private final AdbTransport mTransport; @NonNull private final String mHost; @@ -54,14 +52,14 @@ public class AdbConnection implements Closeable { private int mLastLocalId; /** - * The input stream that this class uses to read from the socket. + * The input stream that this class uses to read from the transport. */ @GuardedBy("lock") @NonNull private final InputStream mPlainInputStream; /** - * The output stream that this class uses to read from the socket. + * The output stream that this class uses to write to the transport. */ @GuardedBy("lock") @NonNull @@ -182,31 +180,66 @@ public static AdbConnection create(@NonNull String host, int port, @NonNull Priv @WorkerThread @NonNull static AdbConnection create(@NonNull String host, int port, @NonNull KeyPair keyPair, int api) throws IOException { - return new AdbConnection(host, port, keyPair, api); + return new AdbConnection(host, port, new SocketAdbTransport(host, port), keyPair, api); + } + + /** + * Creates an ADB connection over an already connected duplex transport. + * The returned connection owns {@code transport} and closes it from + * {@link #close()}. + */ + @WorkerThread + @NonNull + public static AdbConnection create(@NonNull AdbTransport transport, @NonNull PrivateKey privateKey, + @NonNull Certificate certificate) + throws IOException { + return create(transport, privateKey, certificate, Build.VERSION_CODES.BASE); + } + + /** + * Creates an ADB connection over an already connected duplex transport. + */ + @WorkerThread + @NonNull + public static AdbConnection create(@NonNull AdbTransport transport, @NonNull PrivateKey privateKey, + @NonNull Certificate certificate, int api) + throws IOException { + return new AdbConnection( + "transport", + 0, + Objects.requireNonNull(transport), + new KeyPair(Objects.requireNonNull(privateKey), Objects.requireNonNull(certificate)), + api + ); } /** * Internal constructor to initialize some internal state */ @WorkerThread - private AdbConnection(@NonNull String host, int port, @NonNull KeyPair keyPair, int api) throws IOException { + private AdbConnection(@NonNull String host, int port, @NonNull AdbTransport transport, + @NonNull KeyPair keyPair, int api) throws IOException { this.mHost = Objects.requireNonNull(host); this.mPort = port; this.mApi = api; this.mProtocolVersion = AdbProtocol.getProtocolVersion(mApi); this.mMaxData = AdbProtocol.getMaxData(api); this.mKeyPair = Objects.requireNonNull(keyPair); + this.mTransport = Objects.requireNonNull(transport); + InputStream plainInputStream; + OutputStream plainOutputStream; try { - this.mSocket = new Socket(host, port); - } catch (Throwable th) { - //noinspection UnnecessaryInitCause - throw (IOException) new IOException().initCause(th); + plainInputStream = Objects.requireNonNull(mTransport.getInputStream()); + plainOutputStream = Objects.requireNonNull(mTransport.getOutputStream()); + } catch (IOException | RuntimeException e) { + try { + mTransport.close(); + } catch (IOException ignored) { + } + throw e; } - this.mPlainInputStream = mSocket.getInputStream(); - this.mPlainOutputStream = mSocket.getOutputStream(); - - // Disable Nagle because we're sending tiny packets - mSocket.setTcpNoDelay(true); + this.mPlainInputStream = plainInputStream; + this.mPlainOutputStream = plainOutputStream; this.mOpenedStreams = new ConcurrentHashMap<>(); this.mLastLocalId = 0; @@ -236,7 +269,7 @@ private Thread createConnectionThread() { loop: while (!mConnectionThread.isInterrupted()) { try { - // Read and parse a message off the socket's input stream + // Read and parse a message from the transport. AdbProtocol.Message msg = AdbProtocol.Message.parse(getInputStream(), mProtocolVersion, mMaxData); switch (msg.command) { @@ -278,12 +311,16 @@ private Thread createConnectionThread() { break; } case AdbProtocol.A_STLS: { + if (!(mTransport instanceof SocketAdbTransport)) { + throw new IOException("ADB TLS upgrade requires a socket transport."); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { sendPacket(AdbProtocol.generateStls()); SSLContext sslContext = SslUtils.getSslContext(mKeyPair); + Socket socket = ((SocketAdbTransport) mTransport).getSocket(); SSLSocket tlsSocket = (SSLSocket) sslContext.getSocketFactory() - .createSocket(mSocket, mHost, mPort, true); + .createSocket(socket, mHost, mPort, true); tlsSocket.startHandshake(); Log.d(TAG, "Handshake succeeded."); @@ -409,10 +446,10 @@ public boolean isConnectionEstablished() { } /** - * Whether the underlying socket is connected to an ADB daemon and is not in a closed state. + * Whether the underlying transport is open. */ public boolean isConnected() { - return !mSocket.isClosed() && mSocket.isConnected(); + return mTransport.isOpen(); } /** @@ -420,7 +457,7 @@ public boolean isConnected() { * fails. * * @return {@code true} if the connection was established, or {@code false} if the connection timed out - * @throws IOException If the socket fails while connecting + * @throws IOException If the transport fails while connecting * @throws InterruptedException If timeout has reached * @throws AdbPairingRequiredException If ADB lacks pairing */ @@ -436,7 +473,7 @@ public boolean connect() throws IOException, InterruptedException, AdbPairingReq * @param throwOnUnauthorised Whether to throw an {@link AdbAuthenticationFailedException} * if the peer rejects out first authentication attempt * @return {@code true} if the connection was established, or {@code false} if the connection timed out - * @throws IOException If the socket fails while connecting + * @throws IOException If the transport fails while connecting * @throws InterruptedException If timeout has reached * @throws AdbAuthenticationFailedException If {@code throwOnUnauthorised} is {@code true} and the peer rejects the * first authentication attempt, which indicates that the peer has not @@ -482,6 +519,20 @@ public AdbStream open(@LocalServices.Services int service, @NonNull String... ar return open(LocalServices.getDestination(service, args)); } + /** + * Opens a service within a bounded timeout shared by connection and stream + * acknowledgement. + */ + @NonNull + public AdbStream open(@LocalServices.Services int service, long timeout, @NonNull TimeUnit unit, + @NonNull String... args) + throws IOException, InterruptedException, AdbPairingRequiredException { + if (service < LocalServices.SERVICE_FIRST || service > LocalServices.SERVICE_LAST) { + throw new IllegalArgumentException("Invalid service: " + service); + } + return open(LocalServices.getDestination(service, args), timeout, unit); + } + /** * Opens an AdbStream object corresponding to the specified destination. * This routine will block until the connection completes. @@ -496,13 +547,32 @@ public AdbStream open(@LocalServices.Services int service, @NonNull String... ar @NonNull public AdbStream open(@NonNull String destination) throws IOException, InterruptedException, AdbPairingRequiredException { - int localId = ++mLastLocalId; + return open(destination, Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } + + /** + * Opens a destination within a bounded timeout shared by connection and + * stream acknowledgement. + * + * @throws SocketTimeoutException If the timeout expires before the peer + * acknowledges the stream. + */ + @NonNull + public AdbStream open(@NonNull String destination, long timeout, @NonNull TimeUnit unit) + throws IOException, InterruptedException, AdbPairingRequiredException { + final long deadlineNanos = deadlineNanos(timeout, Objects.requireNonNull(unit)); + final int localId; + synchronized (this) { + localId = ++mLastLocalId; + } if (!mConnectAttempted) { throw new IllegalStateException("connect() must be called first"); } - waitForConnection(Long.MAX_VALUE, TimeUnit.MILLISECONDS); + if (!waitForConnectionUntil(deadlineNanos)) { + throw new SocketTimeoutException("Timed out waiting for the ADB connection."); + } // Add this stream to this list of half-open streams AdbStream stream = new AdbStream(this, localId); @@ -512,8 +582,20 @@ public AdbStream open(@NonNull String destination) sendPacket(AdbProtocol.generateOpen(localId, Objects.requireNonNull(destination))); // Wait for the connection thread to receive the OKAY - synchronized (stream) { - stream.wait(); + try { + synchronized (stream) { + while (!stream.isOpenAcknowledged() && !stream.isClosed()) { + long remainingNanos = remainingNanos(deadlineNanos); + if (remainingNanos == 0) { + closeHalfOpenStream(localId, stream); + throw new SocketTimeoutException("Timed out waiting for the ADB stream to open."); + } + timedWait(stream, remainingNanos); + } + } + } catch (InterruptedException e) { + closeHalfOpenStream(localId, stream); + throw e; } // Check if the OPEN request was rejected @@ -527,11 +609,19 @@ public AdbStream open(@NonNull String destination) private boolean waitForConnection(long timeout, @NonNull TimeUnit unit) throws InterruptedException, IOException, AdbPairingRequiredException { + return waitForConnectionUntil(deadlineNanos(timeout, Objects.requireNonNull(unit))); + } + + private boolean waitForConnectionUntil(long deadlineNanos) + throws InterruptedException, IOException, AdbPairingRequiredException { synchronized (this) { // Block if a connection is pending, but not yet complete - long timeoutEndMillis = System.currentTimeMillis() + Objects.requireNonNull(unit).toMillis(timeout); - while (!mConnectionEstablished && mConnectAttempted && timeoutEndMillis - System.currentTimeMillis() > 0) { - wait(timeoutEndMillis - System.currentTimeMillis()); + while (!mConnectionEstablished && mConnectAttempted) { + long remainingNanos = remainingNanos(deadlineNanos); + if (remainingNanos == 0) { + return false; + } + timedWait(this, remainingNanos); } if (!mConnectionEstablished) { @@ -549,6 +639,7 @@ private boolean waitForConnection(long timeout, @NonNull TimeUnit unit) throw (AdbPairingRequiredException) (new AdbPairingRequiredException("ADB pairing is required.").initCause(connectionException)); } } + throw (IOException) new IOException("Connection failed").initCause(connectionException); } throw new IOException("Connection failed"); } @@ -558,6 +649,42 @@ private boolean waitForConnection(long timeout, @NonNull TimeUnit unit) return true; } + private void closeHalfOpenStream(int localId, @NonNull AdbStream stream) { + mOpenedStreams.remove(localId, stream); + try { + stream.close(); + } catch (IOException ignored) { + } + } + + private static long deadlineNanos(long timeout, @NonNull TimeUnit unit) { + if (timeout < 0) { + throw new IllegalArgumentException("timeout < 0"); + } + long now = System.nanoTime(); + long duration = unit.toNanos(timeout); + if (duration == Long.MAX_VALUE || now > Long.MAX_VALUE - duration) { + return Long.MAX_VALUE; + } + return now + duration; + } + + private static long remainingNanos(long deadlineNanos) { + if (deadlineNanos == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + return Math.max(0, deadlineNanos - System.nanoTime()); + } + + private static void timedWait(@NonNull Object monitor, long remainingNanos) + throws InterruptedException { + if (remainingNanos == Long.MAX_VALUE) { + monitor.wait(); + } else { + TimeUnit.NANOSECONDS.timedWait(monitor, remainingNanos); + } + } + /** * This function terminates all I/O on streams associated with this ADB connection */ @@ -573,14 +700,14 @@ private void cleanupStreams() { } /** - * This routine closes the Adb connection and underlying socket + * This routine closes the ADB connection and underlying transport. * - * @throws IOException if the socket fails to close + * @throws IOException if the transport fails to close */ @Override public void close() throws IOException { - // Closing the socket will kick the connection thread - mSocket.close(); + // Closing the transport must kick the connection thread. + mTransport.close(); // Wait for the connection thread to die mConnectionThread.interrupt(); @@ -618,6 +745,7 @@ public static class Builder { private Certificate mCertificate; private KeyPair mKeyPair; private String mDeviceName; + private AdbTransport mTransport; public Builder() { } @@ -643,6 +771,15 @@ public Builder setPort(int port) { return this; } + /** + * Use an already connected duplex transport instead of opening a TCP + * socket. The resulting connection owns and closes the transport. + */ + public Builder setTransport(@NonNull AdbTransport transport) { + this.mTransport = Objects.requireNonNull(transport); + return this; + } + /** * Set a name for the device. Default is “Unknown Device”. * @@ -687,9 +824,9 @@ Builder setKeyPair(KeyPair keyPair) { } /** - * Creates a new {@link AdbConnection} associated with the socket and crypto object specified. + * Creates a new {@link AdbConnection} using the configured transport and keys. * - * @throws IOException If there was an error while establishing a socket connection + * @throws IOException If there was an error while initializing the transport */ public AdbConnection build() throws IOException { if (mKeyPair == null) { @@ -698,7 +835,12 @@ public AdbConnection build() throws IOException { } mKeyPair = new KeyPair(mPrivateKey, mCertificate); } - AdbConnection adbConnection = create(mHost, mPort, mKeyPair, mApi); + AdbConnection adbConnection; + if (mTransport != null) { + adbConnection = new AdbConnection("transport", 0, mTransport, mKeyPair, mApi); + } else { + adbConnection = create(mHost, mPort, mKeyPair, mApi); + } if (mDeviceName != null) { adbConnection.setDeviceName(mDeviceName); } @@ -710,16 +852,21 @@ public AdbConnection build() throws IOException { * attempt fails. * * @return The underlying {@link AdbConnection} - * @throws IOException If the socket fails while connecting + * @throws IOException If the transport fails while connecting * @throws InterruptedException If timeout has reached * @throws AdbPairingRequiredException If ADB lacks pairing */ public AdbConnection connect() throws IOException, InterruptedException, AdbPairingRequiredException { AdbConnection adbConnection = build(); - if (adbConnection.connect()) { - throw new IOException("Unable to establish a new connection."); + try { + if (!adbConnection.connect()) { + throw new IOException("Unable to establish a new connection."); + } + return adbConnection; + } catch (IOException | InterruptedException | AdbPairingRequiredException | RuntimeException e) { + closeQuietly(adbConnection); + throw e; } - return adbConnection; } /** @@ -730,7 +877,7 @@ public AdbConnection connect() throws IOException, InterruptedException, AdbPair * @param throwOnUnauthorised Whether to throw an {@link AdbAuthenticationFailedException} * if the peer rejects out first authentication attempt * @return {@code true} if the connection was established, or {@code false} if the connection timed out - * @throws IOException If the socket fails while connecting + * @throws IOException If the transport fails while connecting * @throws InterruptedException If timeout has reached * @throws AdbAuthenticationFailedException If {@code throwOnUnauthorised} is {@code true} and the peer rejects * the first authentication attempt, which indicates that the peer has @@ -740,10 +887,22 @@ public AdbConnection connect() throws IOException, InterruptedException, AdbPair public AdbConnection connect(long timeout, @NonNull TimeUnit unit, boolean throwOnUnauthorised) throws IOException, InterruptedException, AdbPairingRequiredException { AdbConnection adbConnection = build(); - if (adbConnection.connect(timeout, unit, throwOnUnauthorised)) { - throw new IOException("Unable to establish a new connection."); + try { + if (!adbConnection.connect(timeout, unit, throwOnUnauthorised)) { + throw new IOException("Unable to establish a new connection."); + } + return adbConnection; + } catch (IOException | InterruptedException | AdbPairingRequiredException | RuntimeException e) { + closeQuietly(adbConnection); + throw e; + } + } + + private static void closeQuietly(@NonNull AdbConnection adbConnection) { + try { + adbConnection.close(); + } catch (IOException ignored) { } - return adbConnection; } } } diff --git a/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java b/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java index 6d24c06..117d921 100644 --- a/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java +++ b/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java @@ -112,6 +112,10 @@ void updateRemoteId(int remoteId) { this.mRemoteId = remoteId; } + boolean isOpenAcknowledged() { + return mRemoteId != 0; + } + /** * Called by the connection thread to indicate the stream is okay to send data. */ diff --git a/libadb/src/main/java/io/github/muntashirakon/adb/AdbTransport.java b/libadb/src/main/java/io/github/muntashirakon/adb/AdbTransport.java new file mode 100644 index 0000000..0749272 --- /dev/null +++ b/libadb/src/main/java/io/github/muntashirakon/adb/AdbTransport.java @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0) + +package io.github.muntashirakon.adb; + +import androidx.annotation.NonNull; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A connected, full-duplex byte transport for ADB packets. + *
+ * Implementations own both streams. {@link #close()} must be idempotent and
+ * must unblock pending reads and writes.
+ */
+public interface AdbTransport extends Closeable {
+ @NonNull
+ InputStream getInputStream() throws IOException;
+
+ @NonNull
+ OutputStream getOutputStream() throws IOException;
+
+ /**
+ * Returns whether the transport can still perform I/O.
+ */
+ boolean isOpen();
+}
diff --git a/libadb/src/main/java/io/github/muntashirakon/adb/SocketAdbTransport.java b/libadb/src/main/java/io/github/muntashirakon/adb/SocketAdbTransport.java
new file mode 100644
index 0000000..3656d1f
--- /dev/null
+++ b/libadb/src/main/java/io/github/muntashirakon/adb/SocketAdbTransport.java
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0)
+
+package io.github.muntashirakon.adb;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+final class SocketAdbTransport implements AdbTransport {
+ @NonNull
+ private final Socket mSocket;
+
+ SocketAdbTransport(@NonNull String host, int port) throws IOException {
+ Socket socket = new Socket(host, port);
+ try {
+ // Disable Nagle because we're sending tiny packets.
+ socket.setTcpNoDelay(true);
+ } catch (IOException | RuntimeException e) {
+ try {
+ socket.close();
+ } catch (IOException ignored) {
+ }
+ throw e;
+ }
+ mSocket = socket;
+ }
+
+ @NonNull
+ Socket getSocket() {
+ return mSocket;
+ }
+
+ @Override
+ @NonNull
+ public InputStream getInputStream() throws IOException {
+ return mSocket.getInputStream();
+ }
+
+ @Override
+ @NonNull
+ public OutputStream getOutputStream() throws IOException {
+ return mSocket.getOutputStream();
+ }
+
+ @Override
+ public boolean isOpen() {
+ return mSocket.isConnected() && !mSocket.isClosed();
+ }
+
+ @Override
+ public void close() throws IOException {
+ mSocket.close();
+ }
+}
diff --git a/libadb/src/test/java/io/github/muntashirakon/adb/AdbConnectionTransportTest.java b/libadb/src/test/java/io/github/muntashirakon/adb/AdbConnectionTransportTest.java
new file mode 100644
index 0000000..69ccc1d
--- /dev/null
+++ b/libadb/src/test/java/io/github/muntashirakon/adb/AdbConnectionTransportTest.java
@@ -0,0 +1,354 @@
+// SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0)
+
+package io.github.muntashirakon.adb;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class AdbConnectionTransportTest {
+ @Test
+ public void builderAcceptsAndOwnsInjectedTransport() throws Exception {
+ PassiveTransport transport = new PassiveTransport();
+
+ AdbConnection connection = builder(transport).build();
+
+ assertTrue(connection.isConnected());
+ connection.close();
+ assertFalse(connection.isConnected());
+ assertEquals(1, transport.closeCount);
+ }
+
+ @Test
+ public void builderClosesTransportWhenStreamInitializationFails() throws Exception {
+ FailingInitializationTransport transport = new FailingInitializationTransport();
+ try {
+ builder(transport).build();
+ } catch (IOException expected) {
+ assertEquals(1, transport.closeCount);
+ return;
+ }
+ throw new AssertionError("Expected transport initialization to fail");
+ }
+
+ @Test
+ public void builderConnectReturnsEstablishedConnection() throws Exception {
+ ScriptedTransport transport = new ScriptedTransport(Script.CONNECT);
+ AdbConnection connection = null;
+ try {
+ connection = builder(transport).connect(1, TimeUnit.SECONDS, false);
+
+ assertTrue(connection.isConnectionEstablished());
+ } finally {
+ if (connection != null) {
+ connection.close();
+ } else {
+ transport.close();
+ }
+ }
+ }
+
+ @Test
+ public void builderDefaultConnectKeepsItsUnboundedCompatibilityPath() throws Exception {
+ ScriptedTransport transport = new ScriptedTransport(Script.CONNECT);
+ AdbConnection connection = builder(transport).connect();
+ try {
+ assertTrue(connection.isConnectionEstablished());
+ } finally {
+ connection.close();
+ }
+ }
+
+ @Test
+ public void builderConnectClosesTransportAfterTimeout() throws Exception {
+ ScriptedTransport transport = new ScriptedTransport(Script.SILENT);
+ try {
+ builder(transport).connect(20, TimeUnit.MILLISECONDS, false);
+ } catch (IOException expected) {
+ assertFalse(transport.isOpen());
+ return;
+ }
+ throw new AssertionError("Expected connection timeout");
+ }
+
+ @Test
+ public void boundedOpenReturnsAcknowledgedStream() throws Exception {
+ ScriptedTransport transport = new ScriptedTransport(Script.CONNECT_AND_ACK_OPEN);
+ AdbConnection connection = builder(transport).connect(1, TimeUnit.SECONDS, false);
+ try {
+ AdbStream stream = connection.open("tcp:8899", 1, TimeUnit.SECONDS);
+ assertFalse(stream.isClosed());
+ stream.close();
+ } finally {
+ connection.close();
+ }
+ }
+
+ @Test
+ public void openTimesOutWhenRemoteNeverAcknowledgesStream() throws Exception {
+ ScriptedTransport transport = new ScriptedTransport(Script.CONNECT_WITHOUT_OPEN_ACK);
+ AdbConnection connection = builder(transport).connect(1, TimeUnit.SECONDS, false);
+ long startedNanos = System.nanoTime();
+ try {
+ connection.open("tcp:8899", 50, TimeUnit.MILLISECONDS);
+ } catch (SocketTimeoutException expected) {
+ long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedNanos);
+ assertTrue("open exceeded its timeout budget", elapsedMillis < 1_000);
+ return;
+ } finally {
+ connection.close();
+ }
+ throw new AssertionError("Expected stream open to time out");
+ }
+
+ @Test
+ public void injectedTransportRejectsTlsUpgradeWithoutWritingStlsResponse() throws Exception {
+ ScriptedTransport transport = new ScriptedTransport(Script.REQUEST_TLS);
+ try {
+ builder(transport).connect(1, TimeUnit.SECONDS, false);
+ } catch (IOException expected) {
+ assertEquals(
+ "ADB TLS upgrade requires a socket transport.",
+ expected.getCause().getMessage()
+ );
+ assertEquals(1, transport.clientWriteCount);
+ assertFalse(transport.isOpen());
+ return;
+ } finally {
+ if (transport.isOpen()) {
+ transport.close();
+ }
+ }
+ throw new AssertionError("Expected TLS upgrade to be rejected");
+ }
+
+ @Test
+ public void interruptingOpenClosesHalfOpenStream() throws Exception {
+ ScriptedTransport transport = new ScriptedTransport(Script.CONNECT_WITHOUT_OPEN_ACK);
+ AdbConnection connection = builder(transport).connect(1, TimeUnit.SECONDS, false);
+ AtomicReference