Skip to content

Enter Bluetooth headset (SCO) mode when a BT headset is the selected audio device (#723) - #776

Merged
patrickrb merged 2 commits into
devfrom
fix/bt-headset-audio-device-723
Aug 25, 2026
Merged

Enter Bluetooth headset (SCO) mode when a BT headset is the selected audio device (#723)#776
patrickrb merged 2 commits into
devfrom
fix/bt-headset-audio-device-723

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

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-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 (a stale saved device id can't force SCO with nothing paired).
  • 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.

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 in MainViewModel'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

…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

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.36%. Comparing base (551b0a2) to head (c060df6).

Files with missing lines Patch % Lines
...dio/ks3ckc/ft8af/ui/settings/RadioAudioSettings.kt 0.00% 2 Missing ⚠️
...n/kotlin/radio/ks3ckc/ft8af/ComposeMainActivity.kt 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             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     
Flag Coverage Δ
android 16.91% <0.00%> (+0.25%) ⬆️
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...n/kotlin/radio/ks3ckc/ft8af/ComposeMainActivity.kt 2.27% <0.00%> (+0.01%) ⬆️
...dio/ks3ckc/ft8af/ui/settings/RadioAudioSettings.kt 0.16% <0.00%> (-0.01%) ⬇️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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-arg shouldEnterHeadsetMode(...).
  • 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.

Comment thread ft8af/app/src/main/java/com/k1af/ft8af/MainViewModel.java
- 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>
@patrickrb
patrickrb merged commit 2dc46a9 into dev Aug 25, 2026
17 checks passed
@patrickrb
patrickrb deleted the fix/bt-headset-audio-device-723 branch August 25, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants