I was building a proximity-based app — the kind where two phones discover each other over BLE, exchange a small payload, and start a session together. Think of it like AirDrop, but for your own app.
The iOS side was easy. The Android side was going well too, until I tried to test it.
The Android Emulator doesn't support Bluetooth.
The Wall
Not "limited support." Not "experimental." Just nothing. You run a BLE scan on an emulator and it sits there, silent, finding zero devices, forever.
The standard answer is: use a physical device. Which is fine until you're iterating fast, running on a Mac without a USB cable handy, or trying to test a cross-platform handshake between iOS and Android at the same time.
There are workarounds. One involves a USB Bluetooth dongle, a full Python Bluetooth stack called Bumble, sudo access, and a specific Android API level. I spent an afternoon with it. It works, but it's not something you'd wish on anyone.
I wanted something that just worked.
The Idea
The emulator can't do Bluetooth. But my Mac can. And the emulator can reach my Mac over TCP at 10.0.2.2 — the standard emulator host IP.
What if I wrote a small macOS app that:
- Runs a TCP server
- Listens for BLE commands from the emulator
- Performs those commands using the Mac's real Bluetooth radio via CoreBluetooth
- Sends the results back over the same connection
And on the Android side, a library that:
- Detects when it's running in an emulator
- Replaces the BLE transport with TCP calls to the Mac
- Gets out of the way entirely on real devices
No configuration. No USB. No Python. Just a menu bar app and one Gradle dependency.
Building It
The protocol came first. I designed a simple JSON format — one object per line, newline-delimited — that mirrors the standard BLE vocabulary:
{ "command": "startScan", "serviceUuids": ["A1B2C3D4-..."] }
{ "event": "advertisementFound", "address": "2AD28C65-...", "rssi": -62 }
{ "command": "connect", "address": "2AD28C65-..." }
{ "event": "connected", "connectionId": "conn-abc123" }
Commands flow from Android to Mac. Events flow back. Binary values are base64. The whole thing fits in a single JSON spec file.
The Mac app is a SwiftUI menu bar app using MenuBarExtra (macOS 13+). It runs an NWListener TCP server and owns two CoreBluetooth proxies — one CBCentralManager for scanning and connecting, one CBPeripheralManager for advertising. Each connected emulator gets its own session that routes commands to the right proxy and forwards delegate callbacks back as events.
The Android library wraps the whole thing in a BLEBridge class with a callback-based API. Emulator detection uses Build.FINGERPRINT — if it contains "generic" or "emulator", you're on an AVD.
The Bugs That Taught Me Things
Port conflict. I originally used port 7788. Turns out the Android emulator's netsimd daemon already uses 7788 for its own internal gRPC BLE simulation protocol. Multiple phantom connections were hitting my bridge before the Android app even started. Moved to 7877, problem gone.
Bluetooth not ready. CoreBluetooth initializes asynchronously. If a startScan command arrives before CBCentralManager reports .poweredOn, it silently fails. The fix: queue the pending scan and replay it when the state update fires.
Premature service discovery. didDiscoverServices fires before characteristics are fetched. I was emitting servicesDiscovered too early — the Android side would call writeCharacteristic, the Mac would look up the characteristic, find nothing, and silently no-op. Fixed by always waiting for didDiscoverCharacteristicsFor before emitting the event.
App Sandbox. Xcode projects get App Sandbox enabled by default. With sandbox on, both TCP listening and Bluetooth are blocked at the OS level — silently, with no useful error message. Removing it from Signing & Capabilities fixed everything.
The Result
End-to-end, it works like this:
- Mac bridge is running in the menu bar
- Android emulator starts,
BLEBridgeconnects over TCP - Bridge sends
bridgeReady— Android starts scanning - Mac scans via CoreBluetooth, finds an iPhone running in host mode
-
advertisementFoundsent to Android - Android connects, discovers services, writes a join request
- iPhone receives the write, sends a handoff notification
- Android receives the payload
Both directions work — the emulator can scan for real peripherals, or advertise and receive connections from them.
Try It
Mac: Download BLEForEmulator.zip from GitHub Releases, unzip, move to /Applications, open it.
Android: Add to your settings.gradle.kts:
maven { url = uri("https://jitpack.io") }
And to your app's build.gradle.kts:
debugImplementation("com.github.engelon:BLEForEmulator:v0.1.1")
Then:
val bridge = BLEBridge()
bridge.onEvent = { event ->
when (event) {
is BridgeEvent.BridgeReady -> bridge.startScan(listOf(MY_SERVICE_UUID))
is BridgeEvent.AdvertisementFound -> bridge.connect(event.address)
// ...
}
}
bridge.connect()
The full source, protocol spec, and docs are at github.com/engelon/BLEForEmulator.
It's a development tool — not for production, use debugImplementation. But for fast iteration on BLE features without reaching for a physical device every time, it does exactly what I needed.
Top comments (0)