diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt
index 06436561d..0b4927562 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt
@@ -230,6 +230,14 @@ fun AppSettingsScreen(
enabled = state.isPremium
)
+ StyledToggle(
+ label = stringResource(R.string.conversational_awareness_both_pods_only),
+ description = stringResource(R.string.conversational_awareness_both_pods_only_description),
+ checked = state.conversationalAwarenessBothPodsOnlyEnabled,
+ onCheckedChange = viewModel::setConversationalAwarenessBothPodsOnlyEnabled,
+ enabled = state.isPremium,
+ )
+
StyledToggle(
label = stringResource(R.string.relative_conversational_awareness_volume),
description = stringResource(R.string.relative_conversational_awareness_volume_description),
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt
index 2c1716c33..606d7f0b2 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt
@@ -18,6 +18,7 @@ import kotlin.math.roundToInt
data class AppSettingsUiState(
val showPhoneBatteryInWidget: Boolean = false,
val conversationalAwarenessPauseMusicEnabled: Boolean = false,
+ val conversationalAwarenessBothPodsOnlyEnabled: Boolean = false,
val relativeConversationalAwarenessVolumeEnabled: Boolean = true,
val disconnectWhenNotWearing: Boolean = false,
val takeoverWhenDisconnected: Boolean = false,
@@ -137,6 +138,7 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
currentState.copy(
showPhoneBatteryInWidget = sharedPreferences.getBoolean("show_phone_battery_in_widget", false),
conversationalAwarenessPauseMusicEnabled = sharedPreferences.getBoolean("conversational_awareness_pause_music", false),
+ conversationalAwarenessBothPodsOnlyEnabled = sharedPreferences.getBoolean("conversational_awareness_both_pods_only", false),
relativeConversationalAwarenessVolumeEnabled = sharedPreferences.getBoolean("relative_conversational_awareness_volume", true),
disconnectWhenNotWearing = sharedPreferences.getBoolean("disconnect_when_not_wearing", false),
takeoverWhenDisconnected = sharedPreferences.getBoolean("takeover_when_disconnected", false),
@@ -167,6 +169,11 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
_uiState.update { it.copy(conversationalAwarenessPauseMusicEnabled = enabled) }
}
+ fun setConversationalAwarenessBothPodsOnlyEnabled(enabled: Boolean) {
+ sharedPreferences.edit { putBoolean("conversational_awareness_both_pods_only", enabled) }
+ _uiState.update { it.copy(conversationalAwarenessBothPodsOnlyEnabled = enabled) }
+ }
+
fun setRelativeConversationalAwarenessVolumeEnabled(enabled: Boolean) {
sharedPreferences.edit { putBoolean("relative_conversational_awareness_volume", enabled) }
_uiState.update { it.copy(relativeConversationalAwarenessVolumeEnabled = enabled) }
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt
index 0cf08c11d..f6111cd25 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt
@@ -165,6 +165,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var deviceName: String = "AirPods",
var earDetectionEnabled: Boolean = true,
var conversationalAwarenessPauseMusic: Boolean = false,
+ var conversationalAwarenessBothPodsOnly: Boolean = false,
var showPhoneBatteryInWidget: Boolean = true,
var relativeConversationalAwarenessVolume: Boolean = true,
var headGestures: Boolean = true,
@@ -440,6 +441,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (!contains("conversational_awareness_pause_music")) putBoolean(
"conversational_awareness_pause_music", false
)
+ if (!contains("conversational_awareness_both_pods_only")) putBoolean(
+ "conversational_awareness_both_pods_only", false
+ )
if (!contains("personalized_volume")) putBoolean("personalized_volume", false)
if (!contains("automatic_ear_detection")) putBoolean(
"automatic_ear_detection", true
@@ -912,10 +916,19 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
setPackage(packageName)
})
+ // Number of buds currently in the ear (0x00 == in ear).
+ val budsInEar = earDetectionNotification.status.count { it == 0x00.toByte() }
+ val blockedForSinglePod = config.conversationalAwarenessBothPodsOnly && budsInEar < 2
+
if (conversationAwarenessNotification.status == 1.toByte() || conversationAwarenessNotification.status == 2.toByte()) {
- MediaController.startSpeaking()
- } else if (conversationAwarenessNotification.status == 6.toByte() ||conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) {
- MediaController.stopSpeaking()
+ if (blockedForSinglePod) {
+ Log.d("AirPodsParser", "CA startSpeaking() suppressed — both-pods-only is ON and only $budsInEar bud(s) in ear")
+ } else {
+ Log.d("AirPodsParser", "CA startSpeaking() called (budsInEar=$budsInEar, bothPodsOnly=${config.conversationalAwarenessBothPodsOnly})")
+ MediaController.startSpeaking()
+ }
+ } else if (conversationAwarenessNotification.status == 6.toByte() || conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) {
+ MediaController.stopSpeaking() // Never gated — volume must always be restored.
}
Log.d(
@@ -1360,6 +1373,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
conversationalAwarenessPauseMusic = sharedPreferences.getBoolean(
"conversational_awareness_pause_music", false
),
+ conversationalAwarenessBothPodsOnly = sharedPreferences.getBoolean(
+ "conversational_awareness_both_pods_only", false
+ ),
showPhoneBatteryInWidget = sharedPreferences.getBoolean(
"show_phone_battery_in_widget", true
),
@@ -1473,6 +1489,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
"conversational_awareness_pause_music" -> config.conversationalAwarenessPauseMusic =
preferences.getBoolean(key, false)
+ "conversational_awareness_both_pods_only" -> config.conversationalAwarenessBothPodsOnly =
+ preferences.getBoolean(key, false)
+
"show_phone_battery_in_widget" -> {
config.showPhoneBatteryInWidget = preferences.getBoolean(key, true)
widgetMobileBatteryEnabled = config.showPhoneBatteryInWidget
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 3356edc4a..cebdc44a0 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -39,6 +39,8 @@
Reduces to a percentage of the current volume instead of the maximum volume.
Pause Music
When you start speaking, music will be paused.
+ Both AirPods in Ear
+ Conversational Awareness only activates when both AirPods are in your ears. This also suppresses the Pause Music action when wearing a single AirPod.
EXAMPLE
Add widget
Control Noise Control Mode directly from your Home Screen.