Skip to content

Commit b5657d9

Browse files
committed
fix: fix a no-op and race condition for bluetooth SCO
1 parent 088db0a commit b5657d9

File tree

4 files changed

+28
-15
lines changed

4 files changed

+28
-15
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ android {
4747
applicationId = "com.ilseon"
4848
minSdk = 24
4949
targetSdk = 36
50-
versionCode = 125
51-
versionName = "0.41.0"
50+
versionCode = 126
51+
versionName = "0.41.1"
5252

5353
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
5454
}

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
1616
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
1717
<uses-permission android:name="android.permission.RECORD_AUDIO" />
18+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
1819
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
1920
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
2021

app/src/main/java/com/ilseon/data/bluetooth/BluetoothChecker.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.bluetooth.BluetoothProfile
88
import android.content.Context
99
import android.content.pm.PackageManager
1010
import android.os.Build
11+
import android.util.Log
1112
import androidx.core.content.ContextCompat
1213

1314
class BluetoothChecker(private val context: Context) {
@@ -20,6 +21,7 @@ class BluetoothChecker(private val context: Context) {
2021
@SuppressLint("WrongConstant")
2122
fun isHeadsetConnected(): Boolean {
2223
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled) {
24+
Log.d("BluetoothChecker", "Bluetooth adapter null or disabled")
2325
return false
2426
}
2527

@@ -29,16 +31,16 @@ class BluetoothChecker(private val context: Context) {
2931
Manifest.permission.BLUETOOTH_CONNECT
3032
) != PackageManager.PERMISSION_GRANTED
3133
) {
32-
// Cannot check without permission.
33-
// You should handle permission request before calling this.
34+
Log.w("BluetoothChecker", "BLUETOOTH_CONNECT permission not granted")
3435
return false
3536
}
3637
}
3738

3839
// A2DP is the profile for high-quality audio streaming (headphones/speakers)
39-
val headsetConnected = bluetoothAdapter.getProfileConnectionState(BluetoothProfile.A2DP) == BluetoothProfile.STATE_CONNECTED
40-
val headSetProxy = bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET) == BluetoothProfile.STATE_CONNECTED
40+
val a2dpConnected = bluetoothAdapter.getProfileConnectionState(BluetoothProfile.A2DP) == BluetoothProfile.STATE_CONNECTED
41+
val headsetConnected = bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET) == BluetoothProfile.STATE_CONNECTED
4142

42-
return headsetConnected || headSetProxy
43+
Log.d("BluetoothChecker", "A2DP=$a2dpConnected, HFP/HSP=$headsetConnected")
44+
return a2dpConnected || headsetConnected
4345
}
4446
}

app/src/main/java/com/ilseon/service/RecordingService.kt

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,10 @@ class RecordingService : MediaBrowserService() {
283283
// Request audio focus only when actually recording
284284
requestRecordingFocus()
285285

286+
// Start Bluetooth SCO routing if a headset is connected.
287+
// We don't wait for SCO to fully connect — we start recording immediately.
288+
// The system will route audio through the BT mic as soon as SCO is ready.
289+
// At worst, the very first ~200-500ms may come from the phone mic.
286290
startBluetoothRouting()
287291

288292
mediaRecorder = createMediaRecorder(outputFile).apply {
@@ -291,6 +295,7 @@ class RecordingService : MediaBrowserService() {
291295
start()
292296
startTime = System.currentTimeMillis()
293297
totalPausedMillis = 0
298+
Log.d("RecordingService", "startRecording: recording started to ${outputFile.absolutePath}")
294299
updatePlaybackState(true)
295300
startForegroundWithType(createNotification("Capturing thought..."))
296301
} catch (e: Exception) {
@@ -372,10 +377,12 @@ class RecordingService : MediaBrowserService() {
372377

373378
private fun createMediaRecorder(file: File): MediaRecorder {
374379
val hasHeadset = bluetoothChecker.isHeadsetConnected()
380+
val audioSource = if (hasHeadset) MediaRecorder.AudioSource.VOICE_COMMUNICATION else MediaRecorder.AudioSource.MIC
381+
Log.d("RecordingService", "createMediaRecorder: hasHeadset=$hasHeadset, audioSource=$audioSource, isScoStarted=$isScoStarted, scoOn=${audioManager.isBluetoothScoOn}")
375382
return (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) MediaRecorder(this)
376383
else @Suppress("DEPRECATION") MediaRecorder()).apply {
377384
// Using VOICE_COMMUNICATION source is generally better for Bluetooth SCO routing
378-
setAudioSource(if (hasHeadset) MediaRecorder.AudioSource.VOICE_COMMUNICATION else MediaRecorder.AudioSource.MIC)
385+
setAudioSource(audioSource)
379386
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
380387
setAudioEncoder(MediaRecorder.AudioEncoder.AAC_ELD)
381388
setAudioEncodingBitRate(128_000)
@@ -393,12 +400,11 @@ class RecordingService : MediaBrowserService() {
393400

394401
private fun startBluetoothRouting() {
395402
if (bluetoothChecker.isHeadsetConnected()) {
396-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
397-
Log.d("RecordingService", "Starting Bluetooth SCO for legacy API")
398-
audioManager.startBluetoothSco()
399-
audioManager.isBluetoothScoOn = true
400-
isScoStarted = true
401-
}
403+
Log.d("RecordingService", "Starting Bluetooth SCO routing")
404+
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
405+
audioManager.startBluetoothSco()
406+
audioManager.isBluetoothScoOn = true
407+
isScoStarted = true
402408
}
403409
}
404410

@@ -407,16 +413,20 @@ class RecordingService : MediaBrowserService() {
407413
Log.d("RecordingService", "Stopping Bluetooth SCO")
408414
audioManager.stopBluetoothSco()
409415
audioManager.isBluetoothScoOn = false
416+
audioManager.mode = AudioManager.MODE_NORMAL
410417
isScoStarted = false
411418
}
412419
}
413420

414421
private fun getBluetoothAudioDevice(): AudioDeviceInfo? {
415422
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
416-
return devices.find {
423+
Log.d("RecordingService", "Available input devices: ${devices.map { "${it.productName}(type=${it.type})" }}")
424+
val btDevice = devices.find {
417425
it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
418426
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && it.type == AudioDeviceInfo.TYPE_BLE_HEADSET)
419427
}
428+
Log.d("RecordingService", "Bluetooth audio device: ${btDevice?.productName ?: "none"}")
429+
return btDevice
420430
}
421431

422432
private fun createNotification(contentText: String): Notification {

0 commit comments

Comments
 (0)