Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.gradle
build
.idea
.idea
local.properties
*/.cxx/
235 changes: 222 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,35 @@

<img src="https://user-images.githubusercontent.com/847683/143626125-5872bdd8-180e-48bb-a64f-47b3688a086d.png" width="700px" />

A Kotlin/Java library to connect directly to an Android device without an adb binary or an ADB server
A Kotlin/Java library for speaking the ADB protocol directly.
The core `dadb` artifact is a JVM library. Platform-specific transports can be added separately, and
using a host-side `adb` binary or `adb server` remains a normal supported option.

Current platform boundary summary:

- STLS/TLS-capable ADB connection is a general ADB capability, not an Android-only protocol
- Android USB host transport is Android-specific because it depends on Android USB APIs
- Wireless Debugging pairing (`adb pair`) is currently provided through `dadb-android`
- if a host-side `adb` binary is available, the `adb server` path it manages remains a normal, supported backend

Practical guidance:

- on desktop and server platforms, using the platform `adb` binary or `adb server` is often the
simplest operational choice
- on Android, relying on an external `adb` binary is usually harder in practice due to packaging,
ABI management, process lifecycle, USB host integration, and inconsistent authorization behavior
across Android versions and vendor builds
- `dadb-android` exists mainly to make Android-native ADB integrations more app-controlled and
predictable, especially for Wireless Debugging pairing, STLS/TLS transport, and USB host
connections

```kotlin
dependencies {
implementation("dev.mobile:dadb:<version>")
}
```

### Example Usage
### Direct TCP

Connect to `emulator-5554` and install `apkFile`:

Expand All @@ -26,6 +46,182 @@ Dadb.create("localhost", 5555).use { dadb ->

*Note: Connect to the odd adb daemon port (5555), not the even emulator console port (5554)*

### Android USB

If you are running on Android and have access to USB host APIs, add the Android transport module:

```kotlin
implementation("dev.mobile:dadb-android:<version>")
```

Then create a direct USB transport from `UsbManager` and `UsbDevice`:

```kotlin
val dadb = Dadb.create(
transportFactory = UsbTransportFactory(usbManager, usbDevice, "usb:${usbDevice.deviceName}"),
keyPair = AdbKeyPair.readDefault(),
)
```

This transport is Android-only. Desktop JVM users should use the core `dadb` artifact with direct TCP or a host-side `adb` binary / `adb server`.

### Experimental Android Runtime Helper

If your Android app wants a small convenience wrapper around app-private ADB identity storage,
pairing, and Android-specific transports, `dadb-android` also exposes `AdbRuntime`.

This layer is experimental. Prefer the lower-level transport and pairing APIs directly if you want
the smallest and most explicit integration surface.

Why this helper exists mostly for Android:

- desktop environments usually already have a manageable `adb` binary / `adb server` workflow
- Android apps often do not
- shipping or invoking an external `adb` binary inside an Android app is usually more fragile than
using app-controlled transports directly
- in practice, different Android versions, ROMs, and `adb` binary versions may also differ in how
authorization behaves, including repeated authorization prompts or connections that do not behave
consistently across devices
- `dadb-android` is therefore aimed at Android-native integrations where avoiding that external
operational dependency is valuable

`AdbRuntime` can own a single runtime directory for:

- `adbkey`
- `adbkey.pub`

```kotlin
@OptIn(ExperimentalDadbAndroidApi::class)
val runtime = AdbRuntime(File(context.filesDir, "adb_keys"))
val keyPair = runtime.loadOrCreateKeyPair()
val identity = runtime.readIdentity()
val publicKey = identity.publicKey
```

This keeps long-lived ADB identity material in app-private storage instead of scattering it across
preferences or unrelated app settings.

You can also rotate or replace the local ADB identity through the runtime:

```kotlin
@OptIn(ExperimentalDadbAndroidApi::class)
val runtime = AdbRuntime(File(context.filesDir, "adb_keys"))

runtime.regenerateKeyPair()
runtime.replaceKeyPair(privateKeyPem, publicKeyText)
```

You can also inspect the current runtime identity:

```kotlin
@OptIn(ExperimentalDadbAndroidApi::class)
val runtime = AdbRuntime(File(context.filesDir, "adb_keys"))

val identity = runtime.readIdentity()
```

### Experimental Android Wireless Debugging TLS

`dadb` core already supports STLS/TLS upgrade when the transport supports it.
On Android, `dadb-android` provides Android-facing pairing helpers, identity-backed TLS transport,
and an optional callback that reports the observed server TLS public key pin after a successful
handshake.

Important boundary:

- the TLS ADB protocol itself is not Android-specific
- the current pairing/runtime helper layer is Android-specific
- if you already have a host-side `adb` binary and its `adb server`, you can keep using `AdbServer.createDadb(...)` and do not need the Android runtime helper layer

Typical flow:

1. Pair with the device through `AdbRuntime`.
2. Connect later through the same runtime using one STLS-capable transport for both plain ADB and Wireless Debugging.
3. If TLS is established, the runtime reports the observed server identity through `onServerTlsPeerObserved`.
4. Your app can decide whether to ignore it, persist it, compare it to prior observations, or notify the user.

```kotlin
@OptIn(ExperimentalDadbAndroidApi::class)
val runtime = AdbRuntime(File(context.filesDir, "adb_keys"))

runtime.pairWithCode(
host = "192.168.0.10",
port = 37123,
pairingCode = "123456",
).getOrThrow()
```

```kotlin
@OptIn(ExperimentalDadbAndroidApi::class)
val runtime =
AdbRuntime(
storageRoot = File(context.filesDir, "adb_keys"),
options =
AdbRuntimeOptions(
onServerTlsPeerObserved = { identity ->
println("observed TLS peer ${identity.target.authority}: ${identity.observedPinSha256Base64}")
},
),
)
```

```kotlin
@OptIn(ExperimentalDadbAndroidApi::class)
val runtime = AdbRuntime(File(context.filesDir, "adb_keys"))
val dadb = runtime.connectNetworkDadb(host = "192.168.0.10", port = 37099)

dadb.use {
val serial = dadb.shell("getprop ro.serialno").output.trim()
println("connected over tls=${dadb.isTlsConnection()}: $serial")
}
```

Current trust model note:

- `AdbRuntime` does not persist endpoint or peer trust state by itself
- `AdbTlsTrustPolicy.TrustAll` accepts the presented TLS certificate and lets the app observe it
- apps that want TOFU, pin comparison, or identity change notifications should implement that in
their own storage layer using `onServerTlsPeerObserved`

### Experimental Android TLS Trust Policy

`AdbRuntime` also accepts an explicit TLS trust policy.

```kotlin
@OptIn(ExperimentalDadbAndroidApi::class)
val runtime =
AdbRuntime(
storageRoot = File(context.filesDir, "adb_keys"),
options = AdbRuntimeOptions(
tlsTrustPolicy = AdbTlsTrustPolicy.TrustAll,
),
)
```

Available policies:

- `AdbTlsTrustPolicy.TrustAll`
- default
- accept the presented TLS server certificate without endpoint or pin verification
- useful when the app wants to observe the peer identity and make its own trust decision
- `AdbTlsTrustPolicy.Custom`
- provide your own trust manager factory

### Using adb server

If you already have a host-side `adb` binary, you can connect through the `adb server` it manages instead of providing a direct transport.
This is useful on desktop JVM environments where physical devices are already managed by `adb`.
This path is intentionally still a normal supported option: new TLS, Android USB, or pairing work does not replace or deprecate it.
For many desktop and server deployments, it is still the easiest path to operate.

```kotlin
val dadb = AdbServer.createDadb(
adbServerHost = "localhost",
adbServerPort = 5037,
deviceQuery = "host:transport:${serialNumber}"
)
```

### Discover a Device

The following discovers and returns a connected device or emulator. If there are multiple it returns the first one found.
Expand All @@ -41,26 +237,31 @@ Use the following API if you want to list all available devices:
val dadbs = Dadb.list()
```

### Connecting to a physical device

*Prerequisite: Connecting to a physical device requires a running adb server. In most cases, this means that you must have the `adb` binary installed on your machine.*

The `Dadb.discover()` and `Dadb.list()` methods now both support USB-connected devices.
If a host-side `adb` binary / `adb server` is available, `Dadb.discover()` and `Dadb.list()` can also return USB-connected physical devices.

```kotlin
// Both of these will include any USB-connected devices if they are available
val dadb = Dadb.discover()
val dadbs = Dadb.list()
```

If you'd like to connect directly to a physical device via its serial number. Use the following API:
### Custom Transport

If your ADB packets do not travel over TCP, Android USB host APIs, or a host-side `adb` binary / `adb server`, you can still supply your own transport.
This is useful for tunnels, in-process bridges, and other bidirectional byte streams.

```kotlin
val dadb = AdbServer.createDadb(
adbServerHost = "localhost",
adbServerPort = 5037,
deviceQuery = "host:transport:${serialNumber}"
)
val dadb = Dadb.create(
description = "my transport",
keyPair = AdbKeyPair.readDefault(),
) {
SourceSinkAdbTransport(
source = mySource,
sink = mySink,
description = "my transport",
closeable = myTransport,
)
}
```

### Install / Uninstall APK
Expand Down Expand Up @@ -108,6 +309,14 @@ val adbKeyPair = AdbKeyPair.read(privateKeyFile, publicKeyFile)
Dadb.create("localhost", 5555, adbKeyPair)
```

On Android, prefer an app-private runtime directory instead of `~/.android`, for example:

```kotlin
@OptIn(ExperimentalDadbAndroidApi::class)
val runtime = AdbRuntime(File(context.filesDir, "adb_keys"))
val adbKeyPair = runtime.loadOrCreateKeyPair()
```

# License

```
Expand Down
67 changes: 31 additions & 36 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,51 +1,46 @@
import com.adarshr.gradle.testlogger.TestLoggerExtension
import com.adarshr.gradle.testlogger.theme.ThemeType.STANDARD
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath(kotlin("gradle-plugin", "1.9.20"))
classpath("com.vanniktech:gradle-maven-publish-plugin:0.33.0")
}
}

plugins {
id("com.adarshr.test-logger") version("3.2.0") apply(true)
id("com.android.library") version "9.1.0" apply false
id("com.adarshr.test-logger") version "4.0.0" apply false
id("com.vanniktech.maven.publish") version "0.36.0" apply false
kotlin("jvm") version "2.3.20" apply false
}

allprojects {
tasks.withType(JavaCompile::class.java) {
pluginManager.apply("com.adarshr.test-logger")

tasks.withType(JavaCompile::class.java).configureEach {
sourceCompatibility = JavaVersion.VERSION_17.toString()
targetCompatibility = JavaVersion.VERSION_17.toString()
}
tasks.withType(KotlinCompile::class.java) {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalUnsignedTypes"

tasks.withType(KotlinCompile::class.java).configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
optIn.add("kotlin.ExperimentalUnsignedTypes")
}
}
tasks.withType(Task::class) {
project.apply(plugin = "com.adarshr.test-logger")
project.configure<TestLoggerExtension> {
theme = STANDARD
showExceptions = true
showStackTraces = false
showFullStackTraces = false
showCauses = true
slowThreshold = 5000
showSummary = true
showSimpleNames = false
showPassed = true
showSkipped = true
showFailed = true
showOnlySlow = false
showStandardStreams = false
showPassedStandardStreams = false
showSkippedStandardStreams = false
showFailedStandardStreams = true
}

configure<TestLoggerExtension> {
theme = STANDARD
showExceptions = true
showStackTraces = false
showFullStackTraces = false
showCauses = true
slowThreshold = 5000
showSummary = true
showSimpleNames = false
showPassed = true
showSkipped = true
showFailed = true
showOnlySlow = false
showStandardStreams = false
showPassedStandardStreams = false
showSkippedStandardStreams = false
showFailedStandardStreams = true
}
}
Loading