Blog
Engineering

SDK 0.29 on phones: Android meets iPhone on Wi-Fi, and phones reach a box over Bluetooth LE

SDK 0.29 on phones: Android meets iPhone on Wi-Fi, and phones reach a box over Bluetooth LE

TL;DR

SDK 0.29.0 lets Android advertise and browse peer streams on Wi-Fi, so Android phones, iPhones and Python hosts on one network find each other without Wi-Fi Direct. Phones can also start encrypted sessions with Linux or Mac boxes over Bluetooth LE, without pairing.

A team in the field carries a mix of iPhones and Android phones, and the site they work on may have a box in a cabinet that holds the work orders. Version 0.29.0 makes the phones better peers to each other and to that box: Android and iPhone now open a fast peer stream to each other on any Wi-Fi network they share, even one with no internet, and a phone can start an encrypted session with a box over Bluetooth LE, with no pairing step in the system settings.

All of it ships in the React Native package, which is the supported way for a phone app to get these changes today. The early Swift package and Android library are built from the same sources.

Android and iPhone discovery on one Wi-Fi network

From 0.29, Android phones and iPhones find each other on a shared Wi-Fi network. With the peer-stream slot (wifiDirect in the configuration) enabled, Android also advertises and browses _offlineprotocol._tcp over DNS-SD on the Wi-Fi network it is on, with the same record iOS and the Python package publish. Until now that slot was Wi-Fi Direct on Android and Network framework streams on iOS. Those two never met: an iPhone cannot join a Wi-Fi Direct group, and Android did not look for streams on the local network. Now they find each other, and Python hosts on that network find them, with no group:

import { OfflineProtocol } from '@offline-protocol/mesh-sdk';

const protocol = new OfflineProtocol({
  appId: 'field-crew',
  profile: 'crew-member-7',
  transports: {
    wifiDirect: { enabled: true },
  },
});

await protocol.start();

A Wi-Fi network with no internet counts, which is the common case on a site whose uplink is down but whose access points still run. The slot stays up while either the Wi-Fi Direct group or the network is, and leaving one ends only that one’s streams. In the SDK team’s test, a Samsung phone on Android 16 and an iPhone 16 on one Wi-Fi network, with Bluetooth off and the relay unreachable, found each other in about a second in both address orders, set up an encrypted session and exchanged messages both ways. A network that blocks multicast or isolates its clients, as many guest and office networks do, finds nothing, and then phones meet over Bluetooth LE or the relay as before.

Four details matter if you ship this:

  • Permissions. The module declares ACCESS_NETWORK_STATE and CHANGE_WIFI_MULTICAST_STATE, both granted at install with no prompt. The LAN path needs no runtime grant until your app targets Android 17 (API 37). From then on the app must declare ACCESS_LOCAL_NETWORK and request it before start(), or the LAN carrier stays off with a warning diagnostic. The SDK does not declare it for you, because declaring it revokes the default grant that apps targeting 36 and lower hold.
  • The address is visible on the network. Every device on that Wi-Fi network can see the phone’s address, the same exposure as a Bluetooth LE advertisement. There is no switch for the LAN alone; turning wifiDirect off turns off both.
  • Both ends now agree on which stream to keep. When two devices dial each other at once, Android used to keep the newer stream while iOS and Python kept the one the lower address opened, so on a shared network they could reconnect forever. Android now uses the same rule.
  • Dead streams end in seconds, not hours. Android 10 and later use the iOS and Python keepalive: 15 seconds idle, 5 between probes, 3 probes. The OS default was two hours.

Android phones on an older release neither advertise nor browse on the LAN, so they meet a 0.29 phone only inside a Wi-Fi Direct group, as before.

Encrypted Bluetooth LE sessions between a phone and a box

From 0.29, a phone can start an encrypted session with a Linux or Mac box over Bluetooth LE, with no system pairing step. A box running the Python service with Bluetooth LE on advertises to phones through BlueZ on Linux, or CoreBluetooth on a Mac. Before 0.29 an Android phone and a Mac running the service found and verified each other and never formed a session, so with encryption required no message crossed. The Mac rotates its Bluetooth address, the phone kept a link to each address, and the box could not tell which phone had written which fragment.

The box’s peripheral now serves the optional Hello characteristic and binds each central to the address its hello proves, so every fragment is attributed to the phone that wrote it. Android already wrote its hello. The iOS central now writes one too, once per connection, so an iPhone that connects to a box can start a session through that link. There is no system pairing step on either side: the SDK proves each device’s identity itself, and the session is encrypted end to end with MLS.

In the SDK team’s tests a Galaxy M36 on Android 16, running the demo app, formed a session with a Mac in about a second and delivered over Bluetooth LE in 0.174 seconds. In a later run the same phone joined an encrypted group of four with two Raspberry Pi 4s and a laptop and exchanged messages with each of them. No iPhone was in either run, so the iOS side of this is unit-tested and type-checked but not yet proven on a phone.

SDK 0.29 closes dials that never connect, counts each peer once against the connection cap, and gives back the slot of a link that drops. These three Android Bluetooth fixes apply to any app, box or no box:

  • A dial that never connects is closed and retried. A connection attempt that got no callback used to hold its address and its slot for the life of the process, and every later advertisement from that peer was answered with “already connecting”. A dial that has not connected after 20 seconds is now closed, and the next scan dials again.
  • The connection cap counts links, not entries. Android reports the phone’s own client link to a GATT server as a central connecting too, so one link counted twice, and two dials filled a cap of four. Each peer address now counts once.
  • A dropped link gives back its slot. When a link dropped, Android redialed the peer without closing the old connection, and Android frees a client slot only on close. A phone has about 32, so an app that ran long enough on links that come and go used them all, and every new connection failed after that. The old connection is now closed when it drops.

Swift package and Android library status in SDK 0.29

The Swift package and the Android library are both still early. The Swift package is tagged 0.29.0, iOS 13 or later, and its storage providers are still not public, so wiring it up is the application’s job and names may change. The Android library is on Maven Central from 0.29.0: implementation("com.offlineprotocol:offline-protocol-sdk:0.29.0"), Android 7 or later, with the Kotlin bindings, native libraries for four ABIs, and the transport and storage classes the app wires to the engine. The same bindings and libraries are on the GitHub release as offline-protocol-0.29.0-android.zip. For a phone app today, the React Native package is the supported path:

npm install @offline-protocol/mesh-sdk@0.29.0

Upgrading a phone app to SDK 0.29

  • wifiDirect: { enabled: true } on Android now also joins the Wi-Fi network. If you enabled it only for Wi-Fi Direct, expect the phone’s address to appear on the networks it joins.
  • Without NEARBY_WIFI_DEVICES (or fine location on Android 12 and lower), start() used to leave the slot off. It now runs on the Wi-Fi network only, and Wi-Fi Direct stays off until enableTransport('wifiDirect') after the grant.
  • Inside a Wi-Fi Direct group, a client whose address is higher than the group owner’s now reconnects past a half-open stream after keepalive ends it, about 30 seconds on Android 10 and later.

The rest is in UPGRADING §27 and the 0.29.0 changelog. The HTTP front post and the box post cover the service side of 0.29.

Get the package from npm, read the source on GitHub, or talk to us about a mixed fleet of phones.

Frequently asked questions

Why do Android phones and iPhones not find each other on a guest or office Wi-Fi network?

A network that blocks multicast or isolates its clients, as many guest and office networks do, finds nothing. The phones then meet over Bluetooth LE or the relay as before.

Which Android permissions does the SDK's Wi-Fi network path need?

The module declares ACCESS_NETWORK_STATE and CHANGE_WIFI_MULTICAST_STATE, both granted at install with no prompt. Once your app targets Android 17 (API 37), it must also declare ACCESS_LOCAL_NETWORK and request it before start(); the SDK does not declare it for you.

Can I turn off the Wi-Fi network path and keep Wi-Fi Direct?

No. There is no switch for the LAN alone; turning wifiDirect off turns off both, and every device on that Wi-Fi network can see the phone's address.

Do I need to pair a phone with the box in the Bluetooth settings?

No. There is no system pairing step on either side. The SDK proves each device's identity itself, and the session is encrypted end to end with MLS.

Has an iPhone formed a session with a box over Bluetooth LE yet?

Not yet. No iPhone was in the SDK team's runs, so the iOS side is unit-tested and type-checked but not yet proven on a phone. An Android phone formed a session with a Mac in about a second.

How do I add the Offline Protocol Android library with Gradle?

From 0.29.0 it is on Maven Central as implementation("com.offlineprotocol:offline-protocol-sdk:0.29.0"), for Android 7 or later. It is still early, and the React Native package remains the supported path for a phone app today.

More posts
Engineering Oct 13, 2026

SDK 0.29 on a box: every carrier on, sessions through a middle box, and a way to prove it

Engineering Oct 6, 2026

SDK 0.28 on servers and desktops: Python on PyPI and a local API