Theodicean.SharpAdb 0.0.8

There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Theodicean.SharpAdb --version 0.0.8
                    
NuGet\Install-Package Theodicean.SharpAdb -Version 0.0.8
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Theodicean.SharpAdb" Version="0.0.8" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Theodicean.SharpAdb" Version="0.0.8" />
                    
Directory.Packages.props
<PackageReference Include="Theodicean.SharpAdb" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Theodicean.SharpAdb --version 0.0.8
                    
#r "nuget: Theodicean.SharpAdb, 0.0.8"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Theodicean.SharpAdb@0.0.8
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Theodicean.SharpAdb&version=0.0.8
                    
Install as a Cake Addin
#tool nuget:?package=Theodicean.SharpAdb&version=0.0.8
                    
Install as a Cake Tool

Theodicean.SharpAdb

Release CI Sonar Quality Gate Qodana Version

Managed .NET client for the Android Debug Bridge wire protocol. Talks directly to adbd on the remote device. No adb binary, no local adb-server, no native dependencies.

Why

The published official adb binaries does not support all architectures and often has dependencies on native libraries. Additionally, software that wants to use the ADB either needs to bundle the correct binary or rely on the end user to install it on their system.

Theodicean.SharpAdb implements the device protocol directly. Your process opens a TCP socket to the device, runs the CNXN/AUTH handshake, and you get a multiplexed stream over which you can run shell commands, push and pull files, install APKs, and so on.

Status

Minimum device versions, by feature:

  • Shell + helpers (ExecuteAsync, install, properties, input, logcat, …): Android 7 (API 24), via the shell_v2 adbd feature.
  • File transfer (SyncSession): Android 9 (API 28), via the sendrecv_v2 adbd feature.

SyncSession.OpenAsync throws NotSupportedException if the device does not advertise sendrecv_v2; everything else works against Android 7+.

Working:

  • TCP transport (port 5555 after adb tcpip, or any IP:port the device is reachable on)
  • CNXN handshake, banner parsing, max-payload negotiation
  • RSA-2048 authentication (signature path + RSAPUBLICKEY enrollment)
  • STLS upgrade for devices that require TLS on the debug socket. Not supported on macOS if the device requires TLS 1.3 (see the note under wireless pairing below — the underlying cause is the same .NET/macOS limitation, and it's not specific to wireless connections; any STLS peer that mandates TLS 1.3 hits it). TLS 1.2 STLS peers are unaffected on any platform.
  • Multiplexed AdbStream with the per-write OKAY ack the protocol requires. Tolerates a small burst of unacked inbound WRTEs (queue depth 8, not 1) — real adbd was observed occasionally splitting a single logical response across two back-to-back WRTE packets during end-to-end testing against a real emulator, even without the delayed_ack feature negotiated
  • shell,v2,raw: for interactive commands with separate stdout/stderr and exit code (AdbShellResult)
  • exec: for raw byte-stream services (e.g. screencap -p → PNG bytes); no stdout/stderr split or exit code
  • sync: v2 (LST2, LIS2, SND2, RCV2) for file transfer, with 64-bit sizes and full POSIX stat fields
  • Streaming APK install via cmd package install -S <size> - — no /data/local/tmp staging
  • Helpers: reboot, package install/uninstall/list, properties, processes, logcat (raw + parsed), screencap, key events, text input, taps/swipes, app start/stop, port forward
  • Fault propagation from the read loop to open streams and to subsequent OpenAsync calls
  • Wireless pairing with the 6-digit PIN (Android 11+, AdbPairing.PairAsync): SPAKE2 over Ed25519 (BoringSSL's construction, not the RFC 9382 P-256 variant) plus TLS 1.3 via BouncyCastle (needed for the RFC 5705 exported keying material .NET's own TLS stack doesn't expose). Validated end-to-end against a real device on Linux (an Android emulator running genuine AOSP adbd/BoringSSL) — pairing, the subsequent ConnectTcpAsync, and running a shell command all succeeded repeatedly. AdbKnownHosts persists the device GUID pairing returns, for matching against _adb-tls-connect._tcp mDNS instance names (mDNS discovery itself is out of scope for this library — bring your own client).
    • Pairing succeeding does not mean you can then connect on macOS. Pairing uses BouncyCastle directly and works fine there, but the ongoing connection afterward goes through the same STLS/SslStream path noted above, which macOS can't complete against a TLS-1.3-only device. A BouncyCastle-based fix for that path was tried and correctly negotiated TLS 1.3, but hit a separate, real limitation in BouncyCastle's own async API (bcgit/bc-csharp#481: its async Stream methods are sync-over-async, each wrapping a blocking call in a pooled task) that deadlocked this library's long-lived background read loop against foreground writes under .NET's thread-pool starvation behavior. For now, use Linux or Windows to actually connect to a wirelessly-paired device; macOS can still be used to perform the pairing step itself.

Not implemented:

  • USB transport. The protocol layer is transport-agnostic (IAdbTransport), so a USB transport using libusb or a platform-specific API can plug in without touching the rest.
  • mDNS device discovery.
  • Sync v2 transparent compression (Brotli/LZ4/Zstd). Compression flag is sent as 0.

Install

Available on NuGet as Theodicean.SharpAdb.

dotnet add package Theodicean.SharpAdb

Or via PackageReference:

<PackageReference Include="Theodicean.SharpAdb" Version="*" />

Quick start

using Theodicean.SharpAdb;
using Theodicean.SharpAdb.Auth;
using Theodicean.SharpAdb.Services;

// Load the same key adb itself uses, so the device already trusts it.
var pem = File.ReadAllText(Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
    ".android", "adbkey"));
using var key = AdbAuthKey.LoadFromPem(pem);

await using var conn = await AdbConnection.ConnectTcpAsync("192.168.1.42", 5555, [key]);

Console.WriteLine($"connected to {conn.DeviceInfo.Model} ({conn.DeviceInfo.Product})");

var result = await conn.ExecuteAsync("getprop ro.build.version.release");
if (result.IsSuccess)
    Console.WriteLine($"android {result.Stdout.Trim()}");

Authentication

The first time a key is presented to a device, the user has to tap "Allow USB debugging from this computer" on the device screen. After that the device persists the public key in /data/misc/adb/adb_keys and subsequent connections authenticate silently.

// Generate a new key and persist it for reuse
using var key = AdbAuthKey.Generate("alice@workstation");
File.WriteAllText("./mykey.pem", key.ExportPrivateKeyPem());

// First connect: device prompts user, AuthenticationMethod = PublicKey
// Subsequent connects: silent, AuthenticationMethod = Signature
await using var conn = await AdbConnection.ConnectTcpAsync(host, port, [key]);

If you want to fail fast instead of triggering the on-device prompt (useful in tests):

var opts = new AdbConnectOptions { SendPublicKeyOnAuthFailure = false };
await using var conn = await AdbConnection.ConnectTcpAsync(host, port, [key], opts);
// Throws AdbAuthenticationException if the device doesn't already trust the key.

For Android 11+ devices that only support wireless debugging, pair using the 6-digit code shown under Developer Options → Wireless debugging → "Pair device with pairing code" (equivalent to adb pair; validated against Android Simulator — see Status above). On macOS, pairing works but the subsequent ConnectTcpAsync will not — see the macOS note under Status; use Linux or Windows for the actual connection:

using Theodicean.SharpAdb.Pairing;

// host:port and the 6-digit code come from the device's pairing screen; the pairing port is
// NOT the regular ADB debug port.
var result = await AdbPairing.PairAsync("192.168.1.42", 37123, "493719", key);
await AdbKnownHosts.AddAsync(result); // remembers the device GUID for later mDNS matching

// Successful pairing means the device now trusts `key`. Regular connects still go through
// ConnectTcpAsync against the (separate, frequently-changing) debug port shown on-device or
// found via _adb-tls-connect._tcp mDNS. Not supported on macOS (see Status).
await using var conn = await AdbConnection.ConnectTcpAsync("192.168.1.42", 42891, [key]);

Finding the debug port via mDNS

The debug port adbd advertises changes across reboots and each time wireless debugging is toggled, so a real app should resolve it fresh before connecting rather than hardcoding a value from setup. mDNS discovery itself is not part of this library — bring your own client (e.g. Theodicean.Makaretu.Dns.Multicast on NuGet) and query for the service type below. Match the mDNS instance name directly against the GUID AdbKnownHosts stored for that device — that is how real adb itself correlates a discovery result to an already-paired device, no other lookup involved:

using Makaretu.Dns;

// _adb-tls-connect._tcp is the ongoing debug-port service; _adb-tls-pairing._tcp is only
// present while the device's pairing screen is open and isn't needed for regular reconnects.
var sd = await ServiceDiscovery.CreateInstance();
sd.ServiceInstanceDiscovered += async args =>
{
    var instanceName = args.ServiceInstanceName!.Labels[0]; // == the device GUID from pairing
    if (await AdbKnownHosts.ContainsAsync(instanceName))
    {
        // extract host/port from args.Message's SRV + address records, then ConnectTcpAsync
    }
};
await sd.QueryServiceInstances(new DomainName("_adb-tls-connect._tcp"));

File transfer

await using var sync = await SyncSession.OpenAsync(conn);

await using var src = File.OpenRead("./build/app.apk");
await sync.PushAsync(src, "/data/local/tmp/app.apk");

await using var dst = File.Create("./screenshot.png");
await sync.PullAsync("/sdcard/Pictures/screenshot.png", dst);

var stat = await sync.StatAsync("/data/local/tmp");
if (stat.IsDirectory) { /* ... */ }

await foreach (var entry in sync.ListAsync("/sdcard"))
    Console.WriteLine($"{entry.Name} ({entry.Size} bytes)");

Helpers

// Properties
var sdk = await conn.GetPropertyAsync("ro.build.version.sdk");
var all = await conn.GetAllPropertiesAsync();

// Packages — streams the APK through `cmd package install` rather than staging on /data/local/tmp.
// FailureReason can be null when adbd printed "Failure" without a bracketed reason; fall back to
// the raw shell output (Raw.Stdout + Raw.Stderr) so the diagnostic isn't empty.
var install = await conn.InstallAsync("./app.apk");
if (!install.IsSuccess)
    throw new InvalidOperationException(
        install.FailureReason ?? $"install failed: {install.Raw.Stdout}{install.Raw.Stderr}");
var packages = await conn.ListPackagesAsync();
await conn.UninstallAsync("com.example.app");

// Input
await conn.SendKeyEventAsync(KeyCode.Home);
await conn.SendTextAsync("hello world");
await conn.TapAsync(540, 1200);
await conn.SwipeAsync(100, 1500, 100, 500, durationMs: 250);

// App lifecycle
await conn.StartAppAsync("com.example.app");
await conn.StopAppAsync("com.example.app");

// Screencap (PNG bytes)
var png = await conn.CaptureScreenAsync();

// Logcat
await foreach (var entry in conn.LogcatAsync(filterSpec: "*:E"))
    Console.WriteLine($"{entry.Priority} {entry.Tag}: {entry.Message}");

// Port forward (local TCP -> device port). Local port 0 = auto-assign.
await using var fwd = await conn.ForwardPortAsync(localPort: 0, remotePort: 8080);
Console.WriteLine($"http://127.0.0.1:{fwd.LocalPort}/");

// Reboot
await conn.RebootAsync(RebootMode.Recovery);

Tests

Unit tests run without a device:

dotnet test tests/Theodicean.SharpAdb.Tests

Integration tests connect to a real device. Set ADB_HOST (and optionally ADB_KEY_PATH):

ADB_HOST=192.168.1.42:5555 dotnet test tests/Theodicean.SharpAdb.IntegrationTests

If ADB_KEY_PATH is unset, the fixture defaults to ~/.android/adbkey (the same file Google's adb uses). If neither path exists, a fresh key is generated and saved; the first connect will prompt the user to tap "Allow" on the device.

For one-time on-device key authorization, set ADB_RUN_BOOTSTRAP=1 and run the BootstrapKeyOnDevice test.

Pairing integration tests need ADB_PAIR_HOST and ADB_PAIR_CODE (from the device's pairing screen; add ADB_CONNECT_HOST to also verify the paired key can connect — skipped on macOS, see Status):

ADB_PAIR_HOST=192.168.1.42:37123 ADB_PAIR_CODE=493719 dotnet test tests/Theodicean.SharpAdb.IntegrationTests

License

MIT License. See LICENSE.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed.  net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.0.9-pre03 7 10/26/2026
0.0.9-pre02 131 10/14/2026
0.0.9-pre01 84 10/14/2026
0.0.8 115 10/7/2026
0.0.7 198 9/9/2026
0.0.6 147 9/2/2026
0.0.5 153 8/31/2026
0.0.4 297 8/11/2026
0.0.4-pre02 157 8/10/2026
0.0.4-pre01 134 8/9/2026
0.0.3 127 8/8/2026
0.0.3-pre04 127 8/7/2026
0.0.3-pre03 377 6/15/2026
0.0.3-pre02 231 5/15/2026
0.0.3-pre01 126 5/15/2026
0.0.2 205 5/13/2026
0.0.2-pre01 143 5/12/2026
0.0.1 157 5/10/2026
0.0.1-pre01 258 5/3/2026