Another app is holding your Bluetooth link hostage
Chad Spensky

How a family-locator app kept our phone from reconnecting to a laptop, and what we changed so it can’t
Allthenticate turns your phone into the key for your laptop. Walk up, and the phone and laptop find each other over Bluetooth Low Energy (BLE), prove who they are, and the laptop unlocks. Walk away, and it locks. When that works, you never think about it. When it doesn’t, you type a password, and you notice.
A few weeks ago one of our own laptops stopped unlocking after sleep. The phone insisted the laptop was nearby. The laptop insisted no phone was connected. Toggling Bluetooth on the phone fixed it every time. This is the story of what was actually going on, which turned out to be neither the phone nor the laptop.
The symptom
The report was one line: “couldn’t unlock after sleeping. eventually got it to work but Bluetooth seems hung.”
The logs told a stranger story. After the laptop woke, our app tried to reconnect, and every attempt failed the same way: the connection came up at the radio level and was torn down about 130 milliseconds later with error 0x3e, Connection Failed to be Established. The app retried every two seconds, for as long as we let it. The longest run we recorded lasted 13 minutes. The only way out was turning Bluetooth off and on.
Meanwhile, the phone’s links to two other laptops were perfectly healthy.
One radio link, many apps
Here is the piece of Bluetooth that most people, including most app developers, never have to think about.
A BLE connection is between two devices, not between two apps. When two apps on your phone talk to the same peripheral, Android doesn’t open two radio links. It opens one, and both apps share it. Android keeps that link up for as long as any app still holds it.
That’s usually a feature. It becomes a problem when you ask Android to disconnect and it can’t, because someone else is still holding on.
Android will tell you who. adb shell dumpsys bluetooth_manager lists every open link along with the apps holding it. On our phone, for the laptop that wouldn’t reconnect, it said:
Two of those three are Life360, the family-location app, which happened to be installed on this phone. Our app had never heard of it.
What Life360 was doing
Within 2 to 5 seconds of our app connecting to the laptop, both of Life360’s processes attached to the same link. When our app let go, Life360 didn’t, so the link stayed up. In the packet captures, the phone sent nothing to the laptop on Life360’s behalf. It asked Android for the device’s list of services, which Android answered from its cache, and then it simply held the connection.
To be clear about what we did not see: we have no evidence Life360 read anything from our laptop, and it could not have unlocked it. Our protocol only accepts a session signed by the paired phone’s key.
Trying to catch it reading something
“It held the link and did nothing” is an unsatisfying answer, so we built bait. ble-link-watch is a small Python tool with two halves:
A bait peripheral that advertises like a laptop and exposes characteristics worth reading: a canary string, a writable field, a subscribable one, a model and serial number, a battery level. Every read, write and subscription is logged with the phone’s address. There’s a Linux version on BlueZ and a Windows version on Windows’s own Bluetooth stack.
A phone monitor that polls
dumpsys bluetooth_manageroveradband logs which apps hold each link, so the peripheral’s log can be attributed to an app.
What it has shown so far, on one Pixel 10 with Life360 26.35.0:
Life360 joins other apps’ fresh connections, not just ours to one laptop. Every time our app made a new connection, Life360 attached to it 2.3 to 2.8 seconds later: to the Windows laptop, to the Linux machine, and once to a smartwatch that a different app had connected to. It never opened a connection of its own that we saw.
Usually it leaves again within a tenth of a second. On the Linux machine and the watch, it held the link for 34 to 78 milliseconds. Sometimes it stays: it held the Windows laptop’s link for hours on one day, and the Linux machine’s for over half an hour once.
It sends nothing of its own over the air. In every join we captured at packet level, the phone sent no request at all while Life360 was briefly attached. Once, Life360 and our app held the Linux machine’s link together for 16 minutes. The phone wrote 194 times in that stretch, and every write went to the same two characteristics our app writes when it holds the link alone. Whatever Life360 learns comes from Android’s cached copy of the device’s services, such as its name and which services it offers.
It never touched the bait. The bait advertised for over six and a half hours on the Linux machine and 11 minutes on the Windows laptop. Nothing connected to it or read any of its characteristics.
We tested one explanation for the long holds and it didn’t pan out. Windows laptops advertise from a random, rotating address, like a Bluetooth tracker, and Linux machines usually don’t. Switching the Linux machine to a random address didn’t change whether Life360 joined; it joined either way.
Why it does it: looking inside the app
Life360’s Android app is public, so we pulled the copy installed on our own phone and read it. The Bluetooth code lives in a module called nearbydeviceskit, and its class names and log messages aren’t obfuscated. They tell a fairly clear story.
It lists every connected Bluetooth device. A component that logs as
OSConnectedDevicesProviderasks Android for every device with an open Bluetooth LE link, whichever app opened it, and keeps their addresses.It checks each one to see whether it’s a Tile. An
IntrospectingBleClientattaches to each device and compares its services against the two Tile service identifiers,FEEDandFEEC. Its log lines include “Tile services not found!”, “Not a TOA supported device.”, “Introspection returned a null tileId” and “In introspection backoff, skipping re-add”. TOA is the protocol Tile trackers speak, and Life360 owns Tile.Usually the check fails fast, and sometimes it doesn’t. A 34 to 78 millisecond attach with no radio traffic is what a check against Android’s cached service list looks like. The app also logs “Introspection exceeded max lifetime while still pending; forcing disconnect”. So a check that never resolves is held until a lifetime limit that isn’t in the app and is probably configured remotely. That fits the multi-hour holds, but we haven’t proved it.
It doesn’t appear to store other devices. The module’s database tables describe Life360’s own hardware only: Tile IDs, authentication keys, firmware, settings and diagnostics. Other devices show up only as in-memory “ignored” and “cached” addresses.
So the holds are almost certainly a side effect of Life360 checking whether each device near you is one of its trackers, on links it borrows from other apps.
One more Bluetooth reader is worth knowing about. A BluetoothDriveDetector listens for Android’s system-wide “a Bluetooth device connected” broadcast. It reads the name of every device that connects, matches it against a remotely configured list of car names to tell when you start driving, and writes the name to the app’s own log. We can’t tell from the app whether those logs leave the phone.
What else is in there
Reading the app for its Bluetooth code turns up the rest of it too. Among the third-party SDKs bundled into Life360 26.35.0:
Arity, Allstate’s driving-data company, with a full trip engine: trip detection, GPS trail, speed, accelerometer, gyroscope and barometer readings, phone lock and unlock during trips, and collision detection. It uploads per-trip data to
tracking.arity.com. An upload helper sends a field namedadid, which suggests the phone’s advertising ID goes with it.Sentiance, a driving and crash-detection SDK. It prepares trips, place visits, crash reports and “were you the driver?” answers for upload to
api.sentiance.com.UID2 and EUID, the ad industry’s shared identity. When a remote flag called
ad_uid2.0_enabledis on, the app normalises an email address, hashes it with SHA-256, and exchanges it with the UID2 service for an advertising token that Google’s ad mediation passes to advertisers. A hashed email isn’t anonymous: anyone who knows your address computes the same hash, which is the point.A full advertising and attribution stack, including Google AdMob and DoubleClick, Vungle, Braze, AppsFlyer and Branch, plus SDKs for bank linking (Plaid) and ID-document verification (Persona).
Life360’s privacy policy does describe collecting driving, location and Bluetooth data and sharing some of it with partners. It’s just a lot to sit behind the Bluetooth permission you granted so your family could see where you are.
Why that breaks reconnection
Holding a link is harmless until the app that made it wants to leave and come back.
When our app disconnected, because the laptop went quiet during sleep or because the app itself was restarted, Android kept the link up for Life360. The laptop, still seeing a live connection from the phone, refused a second one. And Windows makes this worse in a specific way: a laptop advertises under a new random Bluetooth address every 30 seconds for privacy. Our app saw the new address, dialled it, and was refused, over and over. Without bonding, there’s no way for an app to tell that the new address and the held link belong to the same laptop.
So the phone was connected to the laptop the whole time, just not in any way our app could use.
What we changed
The fixes are on both ends:
Keep dialling the link that’s still up. On Android the app now follows the operating system’s link events. While Android reports the old link as still alive, the app redials that device instead of the laptop’s newest address, and Android joins the new connection to the live link.
Find a held link we never had. If the app has just started, it has no idea which link is the laptop’s. It now asks the OS for every open Bluetooth connection, skips the ones it already owns, checks each remaining one for our service, and joins it. After the handshake it confirms the laptop’s identity. A link to the wrong laptop is dropped, and so is a link to someone’s smartwatch.
Let the laptop start over on a live link. When our app restarts on a link that never dropped, the laptop still holds the old session and would ignore the new handshake. The laptop service now treats a fresh handshake from the same phone as a restart, and starts a new session on the existing link.
Tell the phone before sleeping. The original trigger was sleep. On Windows’ modern standby, our service never heard the laptop was going to sleep, so the phone’s idle timer tore the link down mid-sleep and handed it to Life360. The service now detects standby from the display turning off, tells the phone, and the phone keeps the link through sleep instead of dropping it.
In our testing, the same restart that used to leave the phone stuck until Bluetooth was toggled now recovers over BLE: about 10 seconds on the Android phone we tested and 15 to 20 seconds on an iPhone. With the sleep fix, the phone never lets go at all, and the laptop unlocks within a second of waking.
Takeaways for anyone building on BLE
Your app does not own its Bluetooth link. Any app on the phone with the Bluetooth permission can attach to it and keep it alive. Design your reconnect logic for a link that refuses to die.
Look at
dumpsys bluetooth_managerearly. The ACL holders line would have saved us a day.Rotating addresses and shared links interact badly. If your peripheral uses resolvable private addresses and your phone isn’t bonded, a held link looks like a device that stopped answering.
The peripheral can’t tell apps apart. Our laptop sees the phone, not the app. Every app on that phone shares one address and one link. Authenticate at the protocol layer, because the link tells you nothing about who is on the other end.
Measure before you theorise. Every fix here came from a packet capture, not a guess.
If you’ve seen “BLE link never dropped” or endless error 147 reconnects in your own crash reports, check who else is holding the link.
Try it yourself
Everything above can be reproduced with a Linux laptop or a Windows machine, an Android phone with USB debugging, and ble-link-watch:
If an app you don’t expect shows up holding your device’s link, or reading its characteristics, we’d like to hear about it.