Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.AACPManager
import me.kavishdevar.librepods.presentation.components.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledToggle
import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
Expand All @@ -46,12 +47,18 @@ fun MicrophoneSettingsRoute(
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceContainer)
) {
val voiceTriggerId = AACPManager.Companion.ControlCommandIdentifiers.VOICE_TRIGGER

MicrophoneSettingsScreen(
selectedMode = state.controlStates[id]?.getOrNull(0)?.toInt() ?: 0,
heySiriDetectionEnabled = state.controlStates[voiceTriggerId]?.getOrNull(0) != 0x02.toByte(),
topPadding = topPadding,
bottomPadding = bottomPadding,
onMicrophoneSettingsChanged = {
viewModel.setControlCommandInt(id, it)
},
onHeySiriDetectionChanged = {
viewModel.setControlCommandBoolean(voiceTriggerId, it)
}
)
}
Expand All @@ -60,9 +67,11 @@ fun MicrophoneSettingsRoute(
@Composable
fun MicrophoneSettingsScreen(
selectedMode: Int,
heySiriDetectionEnabled: Boolean = true,
topPadding: Dp = 16.dp,
bottomPadding: Dp = 16.dp,
onMicrophoneSettingsChanged: (Int) -> Unit
onMicrophoneSettingsChanged: (Int) -> Unit,
onHeySiriDetectionChanged: (Boolean) -> Unit = {}
) {
val scrollState = rememberScrollState()

Expand Down Expand Up @@ -96,6 +105,17 @@ fun MicrophoneSettingsScreen(
)
}

Spacer(modifier = Modifier.height(16.dp))

StyledList {
StyledToggle(
label = stringResource(R.string.hey_siri_detection),
description = stringResource(R.string.hey_siri_detection_description),
checked = heySiriDetectionEnabled,
onCheckedChange = onHeySiriDetectionChanged
)
}

Spacer(modifier = Modifier.height(bottomPadding))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ class AirPodsViewModel(
fun observeAACP() {
val identifiersList = listOf(
ControlCommandIdentifiers.MIC_MODE,
ControlCommandIdentifiers.VOICE_TRIGGER,
ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL,
ControlCommandIdentifiers.CLICK_HOLD_INTERVAL,
ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
registerReceiver(bluetoothReceiver, serviceIntentFilter)
}

val hotwordIntentFilter = IntentFilter(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT).apply {
addCategory("${BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY}.$APPLE")
}
registerReceiver(hotwordReceiver, hotwordIntentFilter, RECEIVER_EXPORTED)

val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter

bluetoothAdapter.bondedDevices.forEach { device ->
Expand Down Expand Up @@ -1185,6 +1190,45 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
})
}

/**
* Receives vendor-specific headset AT events from the Bluetooth stack. When AirPods report
* a "Hey Siri" trigger (+APLSIRI), start the digital assistant, unless the user has turned
* background Hey Siri detection off in Microphone Mode settings.
*
* The event format is not yet confirmed on a real device: every vendor event is logged so
* the actual traffic can be checked against VENDOR_SPECIFIC_HEADSET_EVENT_APLSIRI.
*/
private val hotwordReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null) return
val cmd = intent.getStringExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD)
val cmdType = intent.getIntExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE, -1)
val args = intent.getSerializableExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS)
Log.d(TAG, "Vendor headset event: cmd=$cmd type=$cmdType args=$args")

if (cmd != VENDOR_SPECIFIC_HEADSET_EVENT_APLSIRI) return

if (!isHeySiriDetectionEnabled()) {
Log.d(TAG, "Hey Siri trigger received but background detection is off; ignoring")
return
}

Log.d(TAG, "Hey Siri trigger received; starting digital assistant")
executeStemAction(StemAction.DIGITAL_ASSISTANT)
}
}

/**
* Reads the last VOICE_TRIGGER value reported by (or sent to) the buds.
* Treats an unknown value as enabled, matching the default on the buds.
*/
private fun isHeySiriDetectionEnabled(): Boolean {
val status = aacpManager.getControlCommandStatus(
AACPManager.Companion.ControlCommandIdentifiers.VOICE_TRIGGER
)
return status?.value?.getOrNull(0) != 0x02.toByte()
}

private fun getActionFor(
bud: AACPManager.Companion.StemPressBudType, type: StemPressType
): StemAction? {
Expand Down Expand Up @@ -2227,6 +2271,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
@Suppress("PrivatePropertyName")
private val APPLE = 0x004C

@Suppress("PrivatePropertyName")
private val VENDOR_SPECIFIC_HEADSET_EVENT_APLSIRI = "+APLSIRI"

@Suppress("PrivatePropertyName")
private val ACTION_BATTERY_LEVEL_CHANGED =
"android.bluetooth.device.action.BATTERY_LEVEL_CHANGED"
Expand Down Expand Up @@ -3112,6 +3159,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} catch (e: Exception) {
e.printStackTrace()
}
try {
unregisterReceiver(hotwordReceiver)
} catch (e: Exception) {
e.printStackTrace()
}
try {
unregisterReceiver(externalBroadcastReceiver)
} catch (e: Exception) {
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
<string name="microphone">Microphone</string>
<string name="microphone_mode">Microphone Mode</string>
<string name="microphone_automatic">Automatic</string>
<string name="hey_siri_detection">Hey Siri detection</string>
<string name="hey_siri_detection_description">Start your digital assistant when AirPods hear \"Hey Siri\"</string>
<string name="microphone_always_right">Always Right</string>
<string name="microphone_always_left">Always Left</string>
<string name="answer_call">Answer call</string>
Expand Down