Skip to content

Engineering notes

By Latent Byte

AirPods on a Pixel: "connected", but no audio

Why AirPods Pro showed as connected on a Pixel but played nothing, traced with adb and macOS logs, and the Tasker workaround that fixes it.

AndroidBluetoothdebugging

The setup is a Pixel phone, a MacBook, and a pair of AirPods Pro. The rule we wanted is simple: when the AirPods come out of the case, they should go back to whichever device used them last. When that was the phone, they should play on the phone.

What actually happened: take the AirPods out, and the Pixel shows them as connected. Press play, and the music comes out of the phone speaker. About 30 seconds later the "connected" label disappears and the AirPods end up on the Mac.

Tapping the AirPods in Quick Settings fixed it every time. That turned out to be the most useful clue.

What is supposed to happen

Pairing guides say that after the first pairing, AirPods reconnect to an Android phone on their own when you open the case nearby, like any other Bluetooth earbuds (Yahoo Tech).

There is a condition. AirPods go back to the device they were last connected to (DEV Community). Apple's automatic switching moves them between Apple devices signed in to the same Apple Account, and an Android phone is not part of it (Apple Support). On the Mac, the "Connect to This Mac" setting controls whether the Mac takes the AirPods while you are using another device. Setting it to "When Last Connected to This Mac" turns that off, but as the name says, the Mac still reconnects when it was the last device.

So when the Mac had them last, a manual connect on the phone is expected. Our failures happened with the phone as the last device, which is the case the guides say should just work. We are not the first to hit it. A Pixel 5 owner on Android Central described AirPods Pro that used to reconnect when the case opened and then stopped, while a manual connect still worked. Their AirPods were also paired with a laptop and an iPad. Re-pairing did not help, and the thread ends without a fix (Android Central).

Watching both sides at once

A Bluetooth problem between two hosts is hard to read from either host alone, so we built one watcher script that merged five streams onto a single timeline:

  1. Phone logcat over wireless adb, filtered to link state (aclStateChangeCallback), profile state for A2DP (media) and HFP (calls), active device changes, A2DP stream start, HCI disconnect reasons, and profile connect requests.
  2. A 5-second poll of dumpsys audio and dumpsys bluetooth_manager on the phone: which output device the music stream uses, which device the player's audio track is bound to, and the active A2DP device. Printed only on change.
  3. log stream on the Mac for bluetoothd and audioaccessoryd: Bluetooth power, connections from the AirPods, in-ear changes, and automatic switching requests.
  4. A 4-second poll on the Mac: Bluetooth power, whether the AirPods are connected, and the default audio input and output.
  5. The Mac's built-in microphone, recorded with ffmpeg and reduced to a per-second loudness level. If music was coming out of the phone speaker next to the laptop, the mic heard it. This was ground truth that did not depend on any "connected" label.

The phone clock ran about one second behind the Mac, so phone log times were shifted before merging. Without that, events on the two hosts appeared in the wrong order.

Two small lessons from building it. First, copy the exact log strings from a raw dump before writing filters; our guessed patterns matched nothing. Second, in zsh log is a shell builtin, so the macOS tool has to be called as /usr/bin/log.

What it was not

Audio routing on the phone. When the phone set up the connection itself (a tap in Settings or Quick Settings), everything worked: A2DP came up with AAC, the AirPods became the active device, the player moved to them, and the Mac let go within a second.

The Mac stealing the AirPods. This was our first suspicion, and it was wrong. Every connection the Mac received was incoming, started by the AirPods, and it only happened after the phone link had already ended. During some failures the Mac's Bluetooth was turned off entirely.

The root cause: a link with nothing on it

Here is a failing case-out, with the Pixel as the last device:

TimeEvent
14:47:51AirPods page the Pixel. A base Bluetooth link (ACL) comes up.
14:47:51The AirPods run a service discovery query over the link.
-Nobody opens A2DP or HFP. Settings shows "connected". Audio stays local.
14:48:24About 33 s later the AirPods close the link (HCI 0x13, remote terminated).
14:48:33The AirPods connect to the Mac, which does open the audio profiles.

A Bluetooth link and a Bluetooth audio connection are two separate layers. The ACL is the base radio link. Media (A2DP) and calls (HFP) are profiles that run on top of it. When the AirPods page a host, they create the base link and then wait for the host to open the profiles. Android does not open profiles on a link the remote side created. So the phone was telling the truth: it was connected, at the link layer, with zero audio profiles.

The AirPods wait about half a minute, give up, and fall back to the Mac, which handles this case the way they expect.

This adds something the guides leave out. "They reconnect to the last device" describes what the AirPods try. Whether audio follows also depends on what the host does with the link. Here the AirPods did reach out to the phone, and the phone still showed "connected" with nothing playing.

Why a tap always works

When you tap the device in Quick Settings or Settings, those apps call BluetoothAdapter.connectAllEnabledProfiles. That is a system API gated by the BLUETOOTH_PRIVILEGED permission. It tells the stack to open every enabled profile whatever state the link is in, and the AirPods accept a profile connect from a paired host every time.

We also checked whether the per-device toggles mattered, in particular contacts access. They do not. The AirPods have media and calls allowed, and they do not offer contact or message access at all, so that toggle has nothing to act on.

Ordinary apps cannot get BLUETOOTH_PRIVILEGED. Tasker requests it, but it is a signature-level permission, so it is never granted. There is also no adb command that connects a single device's profiles, so a pure adb fix was not an option.

Why the automation app did not help either

The phone already had a Tasker profile that ran Tasker's Bluetooth Connection, Connect action on screen unlock. Two things were wrong with it.

The trigger was wrong. The AirPods give up in about 30 seconds, and you rarely unlock inside that window.

The action did nothing in the broken state. We moved the trigger to "the AirPods' link came up", which fired within about 100 ms, and still nothing reached the Bluetooth stack. Tasker's Connect works when there is no link at all. Run by hand with the AirPods idle, it opened A2DP within about four seconds, and the stack added HFP a few seconds later. With a base link already up, though, it does nothing and reports success.

Its log line made this harder to see. Every run logged BluetoothAdapter: A2DP connected, which reads like a device state. It is only Tasker's connection to the system's A2DP service. Our first conclusion was that Tasker's Connect never worked, and that was wrong too. A manual test with the AirPods idle corrected it.

The workaround: drop the link, then connect

If Connect works only when there is no link, the fix is to remove the link first. Turning Bluetooth off and on does that. Tasker cannot toggle Bluetooth itself, because apps that target Android 13 or newer are not allowed to. The Tasker Settings helper app targets an old SDK and still holds BLUETOOTH_ADMIN, so Tasker's Bluetooth on/off action goes through it.

The final profile:

Profile: Headphones Link Connect
  Trigger: Bluetooth connected to the AirPods' address, not in the car

Task:
  wait 3 s
  %a2dp = BluetoothAdapter.getDefaultAdapter().getProfileConnectionState(2)
  if %a2dp is 1 (connecting) or 2 (connected)
    stop, nothing to fix
  else
    Bluetooth off (through Tasker Settings)
    wait 2 s
    Bluetooth on
    wait 1 s
    Bluetooth Connection: Connect to the AirPods
    wait 20 s

getProfileConnectionState(2) asks whether any A2DP device is connected. Tasker can call it through its Java Function action, which tells a real media connection apart from a bare link. The 20-second wait at the end also works as a guard: while the task is still running, Tasker ignores a second trigger, so the reconnect it causes cannot start a loop.

The first unassisted run, Mac clock:

TimeEvent
16:02:09AirPods page the phone. Link only, no audio.
16:02:09Tasker profile fires.
16:02:12Bluetooth off. The bare link drops.
16:02:15Bluetooth on.
16:02:17Tasker Connect.
16:02:18A2DP connected. Media plays in the AirPods.

Media audio arrived about 9 seconds after the broken link appeared, well inside the AirPods' 33-second window.

What it does not fix

The workaround needs a trigger, and sometimes there is none. On several case-outs the AirPods paged nothing at all, neither the phone nor the Mac. The phone gets no signal, so no phone-side rule can react.

We tried to predict when this happens and got it wrong twice. First, a clean last disconnect looked like it mattered; the next sample disproved it. Then time in the case looked like the answer, until the AirPods paged nothing after less than a minute in the case. With the data we have, whether the AirPods try the Pixel after case-out looks random.

The usual advice for AirPods that stop reconnecting is to update the firmware, re-pair, or reset them. Firmware updates only come through an iPhone, iPad, or Mac (Apple Support), and ours already ran the latest version, 9A348. We re-paired the AirPods with the Pixel partway through the session, and both failure modes showed up before and after. A full reset is the one step we have not tried.

For that case the options are a phone-side trigger such as screen unlock, which pulls the AirPods away from the Mac on every unlock, or an app that watches the case's Bluetooth LE advertisements and connects when the lid opens.

Other trade-offs:

  • The restart drops every Bluetooth device for about three seconds, including a watch or a car.
  • In the one live run, calls (HFP) had not come up by the time the user also tapped the AirPods, so we do not yet know if calls join on their own.
  • It is one unassisted success so far. A few days of normal use will show how often each failure mode happens.

Debugging lessons

  • "Connected" is not one state. Check the profile state, not the label or the link.
  • Verify a tool's log line before trusting it. "A2DP connected" described Tasker's service binding, not the headphones.
  • A Bluetooth restart is not a reconnect. In one early test it looked like one, because the AirPods were in use and reconnected after the unexpected link loss. With idle AirPods it did nothing.
  • A paused task blocks its trigger. We stepped through the task in Tasker's editor and left it paused. When the real trigger fired, Tasker refused to start a second instance and showed a "not running" toast. Press stop after stepping through a task.
  • Re-check your assumptions every round. With the AirPods moving between two hosts, it is easy to lose track of which one was last. We got that wrong once and only caught it in the logs.
  • Use an independent signal. The microphone did not care what any UI said. It just heard whether the phone speaker was playing.