Enter Bluetooth headset (SCO) mode when a BT headset is the selected audio device (#723) - #776
Conversation
…audio device (#723) FT8AF only started Android's Bluetooth SCO (headset) link when the *rig* connection mode was Bluetooth. A user on a USB or VOX rig who selected a Bluetooth headset as the FT8 mic/speaker got no SCO, so the app captured the built-in mic instead and the headset never worked — the known workaround was to launch FT8CN first purely to turn SCO on. SCO must stay gated (opening it knocks a paired car/headphones out of A2DP music), but picking a BT headset as the FT8 audio device is explicit consent to route over it — the signal the old gate was missing. Fix: - ScoPolicy: audioSelectionNeedsHeadsetMode() + a 4-arg shouldEnterHeadsetMode() that enters SCO when a BT rig is in use OR a BT-SCO input/output device is selected, but only while a BT audio profile is actually connected. - AudioDeviceSpinnerAdapter.getDeviceType(position) exposes the AudioDeviceInfo type so the picker can detect a BT-SCO choice. - MainViewModel.refreshBluetoothHeadsetMode(): brings SCO up/down to match the rig + selected devices and rebuilds the AudioRecord so capture binds to the SCO route; guarded so it only toggles on a real change and never yanks SCO from a Bluetooth rig. - Called at launch (replacing the rig-only gate) and from both audio-device pickers. Note: PR #772 (issue #759) reworks how SCO is brought up/retried; this changes *when*. They're complementary — whoever merges second resolves the small MainViewModel overlap. Tests: ScoPolicyAudioDeviceTest (device-type detection + all gate combinations, incl. the framework-constant check and the car-stereo protection). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #776 +/- ##
============================================
+ Coverage 42.29% 42.36% +0.07%
Complexity 226 226
============================================
Files 267 267
Lines 31730 31819 +89
Branches 3634 3649 +15
============================================
+ Hits 13419 13479 +60
- Misses 18054 18082 +28
- Partials 257 258 +1
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 Changes recommended
refreshBluetoothHeadsetMode() can desynchronize from the real SCO state and then fail to re-enable SCO when it drops, which can break the core headset-routing behavior this PR introduces.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes issue #723 by ensuring Android Bluetooth headset (SCO) mode is entered not only for Bluetooth rig mode, but also when the user explicitly selects a Bluetooth-SCO device as the app’s input/output audio device, so the headset mic/speaker actually works on USB/VOX rigs.
Changes:
- Adds audio-device-aware SCO gating via
ScoPolicy.audioSelectionNeedsHeadsetMode()and a 4-argshouldEnterHeadsetMode(...). - Introduces
MainViewModel.refreshBluetoothHeadsetMode()and calls it at launch and when the user changes audio input/output device selections. - Adds unit tests covering the new SCO gating combinations.
File summaries
| File | Description |
|---|---|
| ft8af/app/src/test/java/com/k1af/ft8af/bluetooth/ScoPolicyAudioDeviceTest.java | Adds unit tests for the new audio-device-aware SCO entry decision logic. |
| ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/RadioAudioSettings.kt | Triggers SCO refresh when the user changes input/output device selection. |
| ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ComposeMainActivity.kt | Switches launch-time behavior to delegate SCO decision/reinit to the view model. |
| ft8af/app/src/main/java/com/k1af/ft8af/ui/AudioDeviceSpinnerAdapter.java | Adds getDeviceType(position) to expose AudioDeviceInfo type for a selected row. |
| ft8af/app/src/main/java/com/k1af/ft8af/MainViewModel.java | Adds refreshBluetoothHeadsetMode() to toggle SCO and rebuild AudioRecord based on rig + selected audio devices. |
| ft8af/app/src/main/java/com/k1af/ft8af/bluetooth/ScoPolicy.java | Adds a framework-constant mirror and new decision logic incorporating selected device types. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- refreshBluetoothHeadsetMode(): cross-check the cached btHeadsetModeActive flag against AudioManager.isBluetoothScoOn() via the new pure ScoPolicy.headsetModeAction(). A headset that dropped SCO on its own (or a failed setBlueToothOn()) no longer leaves the flag stuck true and the selected BT mic/speaker dead until restart; SCO that is really up is still not stop/started again on every settings tap, and a Bluetooth rig's SCO is never yanked from here. - AudioDeviceSpinnerAdapter.getDeviceType() Javadoc: only the USB-direct rows return -1; Android-routed USB AudioDeviceInfo rows return their real type. - ScoPolicyAudioDeviceTest: read AudioDeviceInfo.TYPE_BLUETOOTH_SCO reflectively so javac can't inline it into a 7 == 7 tautology; add headsetModeAction tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes #723.
What was wrong
FT8AF only started Android's Bluetooth SCO (headset) link when the rig connection mode was Bluetooth. A user on a USB or VOX rig who selected a Bluetooth headset as the FT8 mic/speaker got no SCO, so the app captured the built-in mic and the headset never worked. The known workaround was to launch FT8CN first purely to turn SCO on, then start FT8AF.
SCO does need to stay gated — opening it knocks a paired car/headphones out of A2DP music (the original "car stereo pause loop" bug) — but picking a BT headset as the FT8 audio device is explicit consent to route over it, which is the signal the old gate was missing.
The fix
ScoPolicy:audioSelectionNeedsHeadsetMode()+ a 4-argshouldEnterHeadsetMode()that enters SCO when a BT rig is in use or a BT-SCO input/output device is selected, but only while a BT audio profile is actually connected (a stale saved device id can't force SCO with nothing paired).AudioDeviceSpinnerAdapter.getDeviceType(position)exposes theAudioDeviceInfotype so the picker can detect a BT-SCO choice.MainViewModel.refreshBluetoothHeadsetMode(): brings SCO up/down to match the rig + selected devices and rebuilds theAudioRecordso capture binds to the SCO route; guarded so it only toggles on a real change and never yanks SCO from a Bluetooth rig.Relationship to PR #772 (issue #759)
#772 reworks how SCO is brought up/retried (ScoLinkTracker, retries, AudioRecord rebuild on
SCO_AUDIO_STATE_CONNECTED). This PR changes when SCO is entered. They're complementary; there's a small overlap inMainViewModel's SCO area that whoever merges second resolves.Tests
ScoPolicyAudioDeviceTest— device-type detection, all gate combinations, the framework-constant check, and the car-stereo protection (USB rig + default audio ⇒ no SCO).Unit tests pass; debug APK builds.
🤖 Generated with Claude Code