diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 5b27d97..c40d78a 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -44,6 +44,11 @@ android {
version = "3.22.1"
}
}
+ sourceSets {
+ getByName("main") {
+ res.srcDirs("src/main/res", "src/main/res-apple")
+ }
+ }
}
dependencies {
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
index 5c2a2bb..3e589b6 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
@@ -124,6 +124,7 @@ import me.kavishdevar.librepods.screens.DebugScreen
import me.kavishdevar.librepods.screens.HeadTrackingScreen
import me.kavishdevar.librepods.screens.HearingAidAdjustmentsScreen
import me.kavishdevar.librepods.screens.HearingAidScreen
+import me.kavishdevar.librepods.screens.HearingProtectionScreen
import me.kavishdevar.librepods.screens.LongPress
import me.kavishdevar.librepods.screens.Onboarding
import me.kavishdevar.librepods.screens.OpenSourceLicensesScreen
@@ -131,6 +132,7 @@ import me.kavishdevar.librepods.screens.RenameScreen
import me.kavishdevar.librepods.screens.TransparencySettingsScreen
import me.kavishdevar.librepods.screens.TroubleshootingScreen
import me.kavishdevar.librepods.screens.UpdateHearingTestScreen
+import me.kavishdevar.librepods.screens.VersionScreen
import me.kavishdevar.librepods.services.AirPodsService
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
import me.kavishdevar.librepods.utils.RadareOffsetFinder
@@ -408,6 +410,12 @@ fun Main() {
composable("update_hearing_test") {
UpdateHearingTestScreen(navController)
}
+ composable("version_info") {
+ VersionScreen(navController)
+ }
+ composable("hearing_protection") {
+ HearingProtectionScreen(navController)
+ }
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/AboutCard.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/AboutCard.kt
new file mode 100644
index 0000000..264f941
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/AboutCard.kt
@@ -0,0 +1,205 @@
+/*
+ * LibrePods - AirPods liberated from Apple’s ecosystem
+ *
+ * Copyright (C) 2025 LibrePods contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+@file:OptIn(ExperimentalEncodingApi::class)
+
+package me.kavishdevar.librepods.composables
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import me.kavishdevar.librepods.R
+import me.kavishdevar.librepods.composables.NavigationButton
+import me.kavishdevar.librepods.services.ServiceManager
+import kotlin.io.encoding.ExperimentalEncodingApi
+
+@Composable
+fun AboutCard(navController: NavController) {
+ val isDarkTheme = isSystemInDarkTheme()
+ val textColor = if (isDarkTheme) Color.White else Color.Black
+ val service = ServiceManager.getService()
+ if (service == null) return
+ val airpodsInstance = service.airpodsInstance
+ if (airpodsInstance == null) return
+ val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+
+ Box(
+ modifier = Modifier
+ .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
+ .padding(horizontal = 16.dp, vertical = 4.dp)
+ ){
+ Text(
+ text = stringResource(R.string.about),
+ style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f)
+ )
+ )
+ }
+
+ val rowHeight = remember { mutableStateOf(0.dp) }
+ val density = LocalDensity.current
+
+ Column(
+ modifier = Modifier
+ .clip(RoundedCornerShape(28.dp))
+ .fillMaxWidth()
+ .background(backgroundColor, RoundedCornerShape(28.dp))
+ .padding(top = 2.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .onGloballyPositioned { coordinates ->
+ rowHeight.value = with(density) { coordinates.size.height.toDp() }
+ },
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.model_name),
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = airpodsInstance.model.displayName,
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ }
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.model_name),
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = airpodsInstance.actualModelNumber,
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ }
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ )
+ val serialNumbers = listOf(
+ airpodsInstance.serialNumber?: "",
+ " ${airpodsInstance.leftSerialNumber}",
+ " ${airpodsInstance.rightSerialNumber}"
+ )
+ val serialNumber = remember { mutableStateOf(0) }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.serial_number),
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ ),
+ )
+ Text(
+ text = serialNumbers[serialNumber.value],
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ ),
+ modifier = Modifier
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null
+ ) {
+ serialNumber.value = (serialNumber.value + 1) % serialNumbers.size
+ }
+ )
+ }
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ )
+ NavigationButton(
+ to = "version_info",
+ navController = navController,
+ name = stringResource(R.string.version),
+ currentState = airpodsInstance.version3,
+ independent = false,
+ height = rowHeight.value + 32.dp
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt
index b4964e8..8a0da0c 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt
@@ -42,15 +42,27 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import me.kavishdevar.librepods.R
+import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.ATTHandles
+import me.kavishdevar.librepods.utils.Capability
import kotlin.io.encoding.ExperimentalEncodingApi
@Composable
fun AudioSettings(navController: NavController) {
val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black
-
+ val service = ServiceManager.getService()
+ if (service == null) return
+ val airpodsInstance = service.airpodsInstance
+ if (airpodsInstance == null) return
+ if (!airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_VOLUME) &&
+ !airpodsInstance.model.capabilities.contains(Capability.CONVERSATION_AWARENESS) &&
+ !airpodsInstance.model.capabilities.contains(Capability.LOUD_SOUND_REDUCTION) &&
+ !airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_AUDIO)
+ ) {
+ return
+ }
Box(
modifier = Modifier
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
@@ -76,52 +88,60 @@ fun AudioSettings(navController: NavController) {
.padding(top = 2.dp)
) {
- StyledToggle(
- label = stringResource(R.string.personalized_volume),
- description = stringResource(R.string.personalized_volume_description),
- controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
- independent = false
- )
+ if (airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_VOLUME)) {
+ StyledToggle(
+ label = stringResource(R.string.personalized_volume),
+ description = stringResource(R.string.personalized_volume_description),
+ controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
+ independent = false
+ )
- HorizontalDivider(
- thickness = 1.dp,
- color = Color(0x40888888),
- modifier = Modifier
- .padding(horizontal= 12.dp)
- )
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ )
+ }
- StyledToggle(
- label = stringResource(R.string.conversational_awareness),
- description = stringResource(R.string.conversational_awareness_description),
- controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
- independent = false
- )
- HorizontalDivider(
- thickness = 1.dp,
- color = Color(0x40888888),
- modifier = Modifier
- .padding(horizontal= 12.dp)
- )
+ if (airpodsInstance.model.capabilities.contains(Capability.CONVERSATION_AWARENESS)) {
+ StyledToggle(
+ label = stringResource(R.string.conversational_awareness),
+ description = stringResource(R.string.conversational_awareness_description),
+ controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
+ independent = false
+ )
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ )
+ }
- StyledToggle(
- label = stringResource(R.string.loud_sound_reduction),
- description = stringResource(R.string.loud_sound_reduction_description),
- attHandle = ATTHandles.LOUD_SOUND_REDUCTION,
- independent = false
- )
- HorizontalDivider(
- thickness = 1.dp,
- color = Color(0x40888888),
- modifier = Modifier
- .padding(horizontal= 12.dp)
- )
+ if (airpodsInstance.model.capabilities.contains(Capability.LOUD_SOUND_REDUCTION)){
+ StyledToggle(
+ label = stringResource(R.string.loud_sound_reduction),
+ description = stringResource(R.string.loud_sound_reduction_description),
+ attHandle = ATTHandles.LOUD_SOUND_REDUCTION,
+ independent = false
+ )
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ )
+ }
- NavigationButton(
- to = "adaptive_strength",
- name = stringResource(R.string.adaptive_audio),
- navController = navController,
- independent = false
- )
+ if (airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_AUDIO)) {
+ NavigationButton(
+ to = "adaptive_strength",
+ name = stringResource(R.string.adaptive_audio),
+ navController = navController,
+ independent = false
+ )
+ }
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryView.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryView.kt
index 7973c75..62893f7 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryView.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryView.kt
@@ -135,6 +135,13 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
val singleDisplayed = remember { mutableStateOf(false) }
+ val airpodsInstance = service.airpodsInstance
+ if (airpodsInstance == null) {
+ return
+ }
+ val budsRes = airpodsInstance.model.budsRes
+ val caseRes = airpodsInstance.model.caseRes
+
Row {
Column (
modifier = Modifier
@@ -142,7 +149,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
horizontalAlignment = Alignment.CenterHorizontally
) {
Image (
- bitmap = ImageBitmap.imageResource(R.drawable.pro_2_buds),
+ bitmap = ImageBitmap.imageResource(budsRes),
contentDescription = stringResource(R.string.buds),
modifier = Modifier
.fillMaxWidth()
@@ -198,7 +205,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
- bitmap = ImageBitmap.imageResource(R.drawable.pro_2_case),
+ bitmap = ImageBitmap.imageResource(caseRes),
contentDescription = stringResource(R.string.case_alt),
modifier = Modifier
.fillMaxWidth()
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/ConnectionSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/ConnectionSettings.kt
index 9bf8417..4d07eea 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/ConnectionSettings.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/ConnectionSettings.kt
@@ -61,7 +61,7 @@ fun ConnectionSettings() {
thickness = 1.dp,
color = Color(0x40888888),
modifier = Modifier
- .padding(horizontal= 12.dp)
+ .padding(horizontal = 12.dp)
)
StyledToggle(
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/HearingHealthSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/HearingHealthSettings.kt
new file mode 100644
index 0000000..725acad
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/HearingHealthSettings.kt
@@ -0,0 +1,109 @@
+/*
+ * LibrePods - AirPods liberated from Apple’s ecosystem
+ *
+ * Copyright (C) 2025 LibrePods contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+@file:OptIn(ExperimentalEncodingApi::class)
+
+package me.kavishdevar.librepods.composables
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import me.kavishdevar.librepods.R
+import me.kavishdevar.librepods.composables.NavigationButton
+import me.kavishdevar.librepods.services.ServiceManager
+import me.kavishdevar.librepods.utils.Capability
+import kotlin.io.encoding.ExperimentalEncodingApi
+
+@Composable
+fun HearingHealthSettings(navController: NavController) {
+ val service = ServiceManager.getService()
+ if (service == null) return
+ val airpodsInstance = service.airpodsInstance
+ if (airpodsInstance == null) return
+ if (airpodsInstance.model.capabilities.contains(Capability.HEARING_AID)) {
+ val isDarkTheme = isSystemInDarkTheme()
+ val textColor = if (isDarkTheme) Color.White else Color.Black
+ val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+
+ if (airpodsInstance.model.capabilities.contains(Capability.PPE)) {
+ Box(
+ modifier = Modifier
+ .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
+ .padding(horizontal = 16.dp, vertical = 4.dp)
+ ){
+ Text(
+ text = stringResource(R.string.hearing_health),
+ style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f)
+ )
+ )
+ }
+ Column(
+ modifier = Modifier
+ .clip(RoundedCornerShape(28.dp))
+ .fillMaxWidth()
+ .background(backgroundColor, RoundedCornerShape(28.dp))
+ .padding(top = 2.dp)
+ ) {
+ NavigationButton(
+ to = "hearing_protection",
+ name = stringResource(R.string.hearing_protection),
+ navController = navController,
+ independent = false
+ )
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ )
+
+ NavigationButton(
+ to = "hearing_aid",
+ name = stringResource(R.string.hearing_aid),
+ navController = navController,
+ independent = false
+ )
+ }
+ } else {
+ NavigationButton(
+ to = "hearing_aid",
+ name = stringResource(R.string.hearing_aid),
+ navController = navController
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/NavigationButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/NavigationButton.kt
index cc43eb5..8d96a54 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/NavigationButton.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/NavigationButton.kt
@@ -47,6 +47,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import me.kavishdevar.librepods.R
@@ -59,7 +60,8 @@ fun NavigationButton(
independent: Boolean = true,
title: String? = null,
description: String? = null,
- currentState: String? = null
+ currentState: String? = null,
+ height: Dp = 58.dp,
) {
val isDarkTheme = isSystemInDarkTheme()
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
@@ -84,7 +86,7 @@ fun NavigationButton(
Row(
modifier = Modifier
.background(animatedBackgroundColor, RoundedCornerShape(if (independent) 28.dp else 0.dp))
- .height(58.dp)
+ .height(height)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt
index 0094ea7..b142816 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt
@@ -95,6 +95,7 @@ import me.kavishdevar.librepods.composables.StyledToggle
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.ATTHandles
+import me.kavishdevar.librepods.utils.Capability
import me.kavishdevar.librepods.utils.RadareOffsetFinder
import kotlin.io.encoding.ExperimentalEncodingApi
@@ -117,6 +118,8 @@ fun AccessibilitySettingsScreen(navController: NavController) {
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
+ val capabilities = remember { ServiceManager.getService()?.airpodsInstance?.model?.capabilities ?: emptySet() }
+
val hearingAidEnabled = remember { mutableStateOf(
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(1) == 0x01.toByte() &&
aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }?.value?.getOrNull(0) == 0x01.toByte()
@@ -371,11 +374,13 @@ fun AccessibilitySettingsScreen(navController: NavController) {
independent = true,
)
- StyledToggle(
- label = stringResource(R.string.loud_sound_reduction),
- description = stringResource(R.string.loud_sound_reduction_description),
- attHandle = ATTHandles.LOUD_SOUND_REDUCTION
- )
+ if (capabilities.contains(Capability.LOUD_SOUND_REDUCTION)) {
+ StyledToggle(
+ label = stringResource(R.string.loud_sound_reduction),
+ description = stringResource(R.string.loud_sound_reduction_description),
+ attHandle = ATTHandles.LOUD_SOUND_REDUCTION
+ )
+ }
if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) {
NavigationButton(
@@ -399,29 +404,31 @@ fun AccessibilitySettingsScreen(navController: NavController) {
independent = true
)
- StyledToggle(
- label = stringResource(R.string.volume_control),
- description = stringResource(R.string.volume_control_description),
- controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE,
- )
+ if (capabilities.contains(Capability.SWIPE_FOR_VOLUME)) {
+ StyledToggle(
+ label = stringResource(R.string.volume_control),
+ description = stringResource(R.string.volume_control_description),
+ controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE,
+ )
- DropdownMenuComponent(
- label = stringResource(R.string.volume_swipe_speed),
- description = stringResource(R.string.volume_swipe_speed_description),
- options = volumeSwipeSpeedOptions.values.toList(),
- selectedOption = selectedVolumeSwipeSpeed?: "Default",
- onOptionSelected = { newValue ->
- selectedVolumeSwipeSpeed = newValue
- aacpManager?.sendControlCommand(
- identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value,
- value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
- ?: 1.toByte()
- )
- },
- textColor = textColor,
- hazeState = hazeState,
- independent = true
- )
+ DropdownMenuComponent(
+ label = stringResource(R.string.volume_swipe_speed),
+ description = stringResource(R.string.volume_swipe_speed_description),
+ options = volumeSwipeSpeedOptions.values.toList(),
+ selectedOption = selectedVolumeSwipeSpeed?: "Default",
+ onOptionSelected = { newValue ->
+ selectedVolumeSwipeSpeed = newValue
+ aacpManager?.sendControlCommand(
+ identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value,
+ value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
+ ?: 1.toByte()
+ )
+ },
+ textColor = textColor,
+ hazeState = hazeState,
+ independent = true
+ )
+ }
if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) {
Text(
@@ -562,88 +569,89 @@ fun AccessibilitySettingsScreen(navController: NavController) {
}
}
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .background(backgroundColor, RoundedCornerShape(28.dp))
- .padding(12.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- for (i in 0 until 8) {
- val eqPhoneValue =
- remember(phoneMediaEQ.value[i]) { mutableFloatStateOf(phoneMediaEQ.value[i]) }
- Row(
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
- .height(38.dp)
- ) {
- Text(
- text = String.format("%.2f", eqPhoneValue.floatValue),
- fontSize = 12.sp,
- color = textColor,
- modifier = Modifier.padding(bottom = 4.dp)
- )
+ // EQ Settings. Don't seem to have an effect?
+ // Column(
+ // modifier = Modifier
+ // .fillMaxWidth()
+ // .background(backgroundColor, RoundedCornerShape(28.dp))
+ // .padding(12.dp),
+ // horizontalAlignment = Alignment.CenterHorizontally
+ // ) {
+ // for (i in 0 until 8) {
+ // val eqPhoneValue =
+ // remember(phoneMediaEQ.value[i]) { mutableFloatStateOf(phoneMediaEQ.value[i]) }
+ // Row(
+ // horizontalArrangement = Arrangement.SpaceBetween,
+ // verticalAlignment = Alignment.CenterVertically,
+ // modifier = Modifier
+ // .fillMaxWidth()
+ // .height(38.dp)
+ // ) {
+ // Text(
+ // text = String.format("%.2f", eqPhoneValue.floatValue),
+ // fontSize = 12.sp,
+ // color = textColor,
+ // modifier = Modifier.padding(bottom = 4.dp)
+ // )
- Slider(
- value = eqPhoneValue.floatValue,
- onValueChange = { newVal ->
- eqPhoneValue.floatValue = newVal
- val newEQ = phoneMediaEQ.value.copyOf()
- newEQ[i] = eqPhoneValue.floatValue
- phoneMediaEQ.value = newEQ
- },
- valueRange = 0f..100f,
- modifier = Modifier
- .fillMaxWidth(0.9f)
- .height(36.dp),
- colors = SliderDefaults.colors(
- thumbColor = thumbColor,
- activeTrackColor = activeTrackColor,
- inactiveTrackColor = trackColor
- ),
- thumb = {
- Box(
- modifier = Modifier
- .size(24.dp)
- .shadow(4.dp, CircleShape)
- .background(thumbColor, CircleShape)
- )
- },
- track = {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(12.dp),
- contentAlignment = Alignment.CenterStart
- )
- {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(4.dp)
- .background(trackColor, RoundedCornerShape(4.dp))
- )
- Box(
- modifier = Modifier
- .fillMaxWidth(eqPhoneValue.floatValue / 100f)
- .height(4.dp)
- .background(activeTrackColor, RoundedCornerShape(4.dp))
- )
- }
- }
- )
+ // Slider(
+ // value = eqPhoneValue.floatValue,
+ // onValueChange = { newVal ->
+ // eqPhoneValue.floatValue = newVal
+ // val newEQ = phoneMediaEQ.value.copyOf()
+ // newEQ[i] = eqPhoneValue.floatValue
+ // phoneMediaEQ.value = newEQ
+ // },
+ // valueRange = 0f..100f,
+ // modifier = Modifier
+ // .fillMaxWidth(0.9f)
+ // .height(36.dp),
+ // colors = SliderDefaults.colors(
+ // thumbColor = thumbColor,
+ // activeTrackColor = activeTrackColor,
+ // inactiveTrackColor = trackColor
+ // ),
+ // thumb = {
+ // Box(
+ // modifier = Modifier
+ // .size(24.dp)
+ // .shadow(4.dp, CircleShape)
+ // .background(thumbColor, CircleShape)
+ // )
+ // },
+ // track = {
+ // Box(
+ // modifier = Modifier
+ // .fillMaxWidth()
+ // .height(12.dp),
+ // contentAlignment = Alignment.CenterStart
+ // )
+ // {
+ // Box(
+ // modifier = Modifier
+ // .fillMaxWidth()
+ // .height(4.dp)
+ // .background(trackColor, RoundedCornerShape(4.dp))
+ // )
+ // Box(
+ // modifier = Modifier
+ // .fillMaxWidth(eqPhoneValue.floatValue / 100f)
+ // .height(4.dp)
+ // .background(activeTrackColor, RoundedCornerShape(4.dp))
+ // )
+ // }
+ // }
+ // )
- Text(
- text = stringResource(R.string.band_label, i + 1),
- fontSize = 12.sp,
- color = textColor,
- modifier = Modifier.padding(top = 4.dp)
- )
- }
- }
- }
+ // Text(
+ // text = stringResource(R.string.band_label, i + 1),
+ // fontSize = 12.sp,
+ // color = textColor,
+ // modifier = Modifier.padding(top = 4.dp)
+ // )
+ // }
+ // }
+ // }
}
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt
index 00ca9e9..adba385 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt
@@ -75,10 +75,12 @@ import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
+import me.kavishdevar.librepods.composables.AboutCard
import me.kavishdevar.librepods.composables.AudioSettings
import me.kavishdevar.librepods.composables.BatteryView
import me.kavishdevar.librepods.composables.CallControlSettings
import me.kavishdevar.librepods.composables.ConnectionSettings
+import me.kavishdevar.librepods.composables.HearingHealthSettings
import me.kavishdevar.librepods.composables.MicrophoneSettings
import me.kavishdevar.librepods.composables.NavigationButton
import me.kavishdevar.librepods.composables.NoiseControlSettings
@@ -91,6 +93,7 @@ import me.kavishdevar.librepods.constants.AirPodsNotifications
import me.kavishdevar.librepods.services.AirPodsService
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
import me.kavishdevar.librepods.utils.AACPManager
+import me.kavishdevar.librepods.utils.Capability
import me.kavishdevar.librepods.utils.RadareOffsetFinder
import kotlin.io.encoding.ExperimentalEncodingApi
@@ -225,6 +228,12 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
snackbarHostState = snackbarHostState
) { spacerHeight, hazeState ->
if (isLocallyConnected || isRemotelyConnected) {
+ val instance = service.airpodsInstance
+ if (instance == null) {
+ Text("Error: AirPods instance is null")
+ return@StyledScaffold
+ }
+ val capabilities = instance.model.capabilities
LazyColumn(
modifier = Modifier
.fillMaxSize()
@@ -248,27 +257,29 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
}
val actAsAppleDeviceHookEnabled = RadareOffsetFinder.isSdpOffsetAvailable()
if (actAsAppleDeviceHookEnabled) {
- item(key = "spacer_hearing_aid") { Spacer(modifier = Modifier.height(32.dp)) }
- item(key = "hearing_aid") {
- NavigationButton(
- to = "hearing_aid",
- name = stringResource(R.string.hearing_aid),
- navController = navController
- )
+ item(key = "spacer_hearing_health") { Spacer(modifier = Modifier.height(32.dp)) }
+ item(key = "hearing_health") {
+ HearingHealthSettings(navController = navController)
}
}
- item(key = "spacer_noise") { Spacer(modifier = Modifier.height(16.dp)) }
- item(key = "noise_control") { NoiseControlSettings(service = service) }
-
- item(key = "spacer_press_hold") { Spacer(modifier = Modifier.height(16.dp)) }
- item(key = "press_hold") { PressAndHoldSettings(navController = navController) }
+ if (capabilities.contains(Capability.LISTENING_MODE)) {
+ item(key = "spacer_noise") { Spacer(modifier = Modifier.height(16.dp)) }
+ item(key = "noise_control") { NoiseControlSettings(service = service) }
+ }
+
+ if (capabilities.contains(Capability.STEM_CONFIG)) {
+ item(key = "spacer_press_hold") { Spacer(modifier = Modifier.height(16.dp)) }
+ item(key = "press_hold") { PressAndHoldSettings(navController = navController) }
+ }
item(key = "spacer_call") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "call_control") { CallControlSettings(hazeState = hazeState) }
- item(key = "spacer_camera") { Spacer(modifier = Modifier.height(16.dp)) }
- item(key = "camera_control") { NavigationButton(to = "camera_control", name = stringResource(R.string.camera_remote), description = stringResource(R.string.camera_control_description), title = stringResource(R.string.camera_control), navController = navController) }
+ if (capabilities.contains(Capability.STEM_CONFIG)) {
+ item(key = "spacer_camera") { Spacer(modifier = Modifier.height(16.dp)) }
+ item(key = "camera_control") { NavigationButton(to = "camera_control", name = stringResource(R.string.camera_remote), description = stringResource(R.string.camera_control_description), title = stringResource(R.string.camera_control), navController = navController) }
+ }
item(key = "spacer_audio") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "audio") { AudioSettings(navController = navController) }
@@ -279,30 +290,37 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
item(key = "spacer_microphone") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "microphone") { MicrophoneSettings(hazeState) }
- item(key = "spacer_sleep") { Spacer(modifier = Modifier.height(16.dp)) }
- item(key = "sleep_detection") {
- StyledToggle(
- label = stringResource(R.string.sleep_detection),
- controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG
- )
+ if (capabilities.contains(Capability.SLEEP_DETECTION)) {
+ item(key = "spacer_sleep") { Spacer(modifier = Modifier.height(16.dp)) }
+ item(key = "sleep_detection") {
+ StyledToggle(
+ label = stringResource(R.string.sleep_detection),
+ controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG
+ )
+ }
}
- item(key = "spacer_head_tracking") { Spacer(modifier = Modifier.height(16.dp)) }
- item(key = "head_tracking") { NavigationButton(to = "head_tracking", name = stringResource(R.string.head_gestures), navController = navController, currentState = if (sharedPreferences.getBoolean("head_gestures", false)) stringResource(R.string.on) else stringResource(R.string.off)) }
+ if (capabilities.contains(Capability.HEAD_GESTURES)) {
+ item(key = "spacer_head_tracking") { Spacer(modifier = Modifier.height(16.dp)) }
+ item(key = "head_tracking") { NavigationButton(to = "head_tracking", name = stringResource(R.string.head_gestures), navController = navController, currentState = if (sharedPreferences.getBoolean("head_gestures", false)) stringResource(R.string.on) else stringResource(R.string.off)) }
+ }
item(key = "spacer_accessibility") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "accessibility") { NavigationButton(to = "accessibility", name = stringResource(R.string.accessibility), navController = navController) }
- item(key = "spacer_off_listening") { Spacer(modifier = Modifier.height(16.dp)) }
- item(key = "off_listening") {
- StyledToggle(
- label = stringResource(R.string.off_listening_mode),
- controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION,
- description = stringResource(R.string.off_listening_mode_description)
- )
+ if (capabilities.contains(Capability.LOUD_SOUND_REDUCTION)){
+ item(key = "spacer_off_listening") { Spacer(modifier = Modifier.height(16.dp)) }
+ item(key = "off_listening") {
+ StyledToggle(
+ label = stringResource(R.string.off_listening_mode),
+ controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION,
+ description = stringResource(R.string.off_listening_mode_description)
+ )
+ }
}
- // an about card- everything but the version number is unknown - will add later if i find out
+ item(key = "spacer_about") { Spacer(modifier = Modifier.height(32.dp)) }
+ item(key = "about") { AboutCard(navController = navController) }
item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "debug") { NavigationButton("debug", "Debug", navController) }
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingProtectionScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingProtectionScreen.kt
new file mode 100644
index 0000000..432f382
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingProtectionScreen.kt
@@ -0,0 +1,90 @@
+/*
+ * LibrePods - AirPods liberated from Apple’s ecosystem
+ *
+ * Copyright (C) 2025 LibrePods contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package me.kavishdevar.librepods.screens
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import com.kyant.backdrop.backdrops.layerBackdrop
+import com.kyant.backdrop.backdrops.rememberLayerBackdrop
+import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import kotlinx.coroutines.Job
+import me.kavishdevar.librepods.R
+import me.kavishdevar.librepods.composables.StyledScaffold
+import me.kavishdevar.librepods.composables.StyledToggle
+import me.kavishdevar.librepods.services.ServiceManager
+import me.kavishdevar.librepods.utils.AACPManager
+import me.kavishdevar.librepods.utils.ATTHandles
+import kotlin.io.encoding.ExperimentalEncodingApi
+
+private var debounceJob: Job? = null
+
+@SuppressLint("DefaultLocale")
+@ExperimentalHazeMaterialsApi
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
+@Composable
+fun HearingProtectionScreen(navController: NavController) {
+ val isDarkTheme = isSystemInDarkTheme()
+ val service = ServiceManager.getService()
+ if (service == null) return
+
+ val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+ val textColor = if (isDarkTheme) Color.White else Color.Black
+
+ val backdrop = rememberLayerBackdrop()
+
+ StyledScaffold(
+ title = stringResource(R.string.hearing_protection),
+ ) { spacerHeight ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .layerBackdrop(backdrop)
+ .padding(horizontal = 16.dp)
+ ) {
+ Spacer(modifier = Modifier.height(spacerHeight))
+
+ StyledToggle(
+ title = stringResource(R.string.environmental_noise),
+ label = stringResource(R.string.loud_sound_reduction),
+ description = stringResource(R.string.loud_sound_reduction_description),
+ attHandle = ATTHandles.LOUD_SOUND_REDUCTION
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+ StyledToggle(
+ title = stringResource(R.string.workspace_use),
+ label = stringResource(R.string.ppe),
+ description = stringResource(R.string.workspace_use_description),
+ controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.PPE_TOGGLE_CONFIG
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/VersionInfoScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/VersionInfoScreen.kt
new file mode 100644
index 0000000..73f7fa6
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/VersionInfoScreen.kt
@@ -0,0 +1,192 @@
+/*
+ * LibrePods - AirPods liberated from Apple’s ecosystem
+ *
+ * Copyright (C) 2025 LibrePods contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package me.kavishdevar.librepods.screens
+
+import androidx.compose.foundation.background
+import android.annotation.SuppressLint
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import com.kyant.backdrop.backdrops.layerBackdrop
+import com.kyant.backdrop.backdrops.rememberLayerBackdrop
+import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import kotlinx.coroutines.Job
+import me.kavishdevar.librepods.R
+import me.kavishdevar.librepods.composables.StyledScaffold
+import me.kavishdevar.librepods.services.ServiceManager
+import kotlin.io.encoding.ExperimentalEncodingApi
+
+private var debounceJob: Job? = null
+
+@SuppressLint("DefaultLocale")
+@ExperimentalHazeMaterialsApi
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
+@Composable
+fun VersionScreen(navController: NavController) {
+ val isDarkTheme = isSystemInDarkTheme()
+ val service = ServiceManager.getService()
+ if (service == null) return
+ val airpodsInstance = service.airpodsInstance
+ if (airpodsInstance == null) return
+
+ val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+ val textColor = if (isDarkTheme) Color.White else Color.Black
+
+ val backdrop = rememberLayerBackdrop()
+
+ StyledScaffold(
+ title = stringResource(R.string.customize_adaptive_audio)
+ ) { spacerHeight ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .layerBackdrop(backdrop)
+ .padding(horizontal = 16.dp)
+ ) {
+ Spacer(modifier = Modifier.height(spacerHeight))
+ Box(
+ modifier = Modifier
+ .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
+ .padding(horizontal = 16.dp, vertical = 4.dp)
+ ){
+ Text(
+ text = stringResource(R.string.version),
+ style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f)
+ )
+ )
+ }
+
+ Column(
+ modifier = Modifier
+ .clip(RoundedCornerShape(28.dp))
+ .fillMaxWidth()
+ .background(backgroundColor, RoundedCornerShape(28.dp))
+ .padding(top = 2.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.version) + " 1",
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = airpodsInstance.version1 ?: "N/A",
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor.copy(0.8f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ }
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.version) + " 2",
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = airpodsInstance.version2 ?: "N/A",
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor.copy(0.8f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ }
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.version) + " 3",
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = airpodsInstance.version3 ?: "N/A",
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor.copy(0.8f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
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 e8dc010..d5045b2 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
@@ -88,6 +88,8 @@ import me.kavishdevar.librepods.constants.StemAction
import me.kavishdevar.librepods.constants.isHeadTrackingData
import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType
+import me.kavishdevar.librepods.utils.AirPodsInstance
+import me.kavishdevar.librepods.utils.AirPodsModels
import me.kavishdevar.librepods.utils.ATTManager
import me.kavishdevar.librepods.utils.BLEManager
import me.kavishdevar.librepods.utils.BluetoothConnectionManager
@@ -152,6 +154,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var localMac = ""
lateinit var aacpManager: AACPManager
var attManager: ATTManager? = null
+ var airpodsInstance: AirPodsInstance? = null
var cameraActive = false
private var disconnectedBecauseReversed = false
private var otherDeviceTookOver = false
@@ -191,6 +194,19 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var rightLongPressAction: StemAction = StemAction.defaultActions[StemPressType.LONG_PRESS]!!,
var cameraAction: AACPManager.Companion.StemPressType? = null,
+
+ // AirPods device information
+ var airpodsName: String = "",
+ var airpodsModelNumber: String = "",
+ var airpodsManufacturer: String = "",
+ var airpodsSerialNumber: String = "",
+ var airpodsLeftSerialNumber: String = "",
+ var airpodsRightSerialNumber: String = "",
+ var airpodsVersion1: String = "",
+ var airpodsVersion2: String = "",
+ var airpodsVersion3: String = "",
+ var airpodsHardwareRevision: String = "",
+ var airpodsUpdaterIdentifier: String = "",
)
private lateinit var config: ServiceConfig
@@ -931,6 +947,49 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
"AirPodsParser",
"Device Information: name: ${deviceInformation.name}, modelNumber: ${deviceInformation.modelNumber}, manufacturer: ${deviceInformation.manufacturer}, serialNumber: ${deviceInformation.serialNumber}, version1: ${deviceInformation.version1}, version2: ${deviceInformation.version2}, hardwareRevision: ${deviceInformation.hardwareRevision}, updaterIdentifier: ${deviceInformation.updaterIdentifier}, leftSerialNumber: ${deviceInformation.leftSerialNumber}, rightSerialNumber: ${deviceInformation.rightSerialNumber}, version3: ${deviceInformation.version3}"
)
+ // Store in SharedPreferences
+ sharedPreferences.edit {
+ putString("airpods_name", deviceInformation.name)
+ putString("airpods_model_number", deviceInformation.modelNumber)
+ putString("airpods_manufacturer", deviceInformation.manufacturer)
+ putString("airpods_serial_number", deviceInformation.serialNumber)
+ putString("airpods_left_serial_number", deviceInformation.leftSerialNumber)
+ putString("airpods_right_serial_number", deviceInformation.rightSerialNumber)
+ putString("airpods_version1", deviceInformation.version1)
+ putString("airpods_version2", deviceInformation.version2)
+ putString("airpods_version3", deviceInformation.version3)
+ putString("airpods_hardware_revision", deviceInformation.hardwareRevision)
+ putString("airpods_updater_identifier", deviceInformation.updaterIdentifier)
+ }
+ // Update config
+ config.airpodsName = deviceInformation.name
+ config.airpodsModelNumber = deviceInformation.modelNumber
+ config.airpodsManufacturer = deviceInformation.manufacturer
+ config.airpodsSerialNumber = deviceInformation.serialNumber
+ config.airpodsLeftSerialNumber = deviceInformation.leftSerialNumber
+ config.airpodsRightSerialNumber = deviceInformation.rightSerialNumber
+ config.airpodsVersion1 = deviceInformation.version1
+ config.airpodsVersion2 = deviceInformation.version2
+ config.airpodsVersion3 = deviceInformation.version3
+ config.airpodsHardwareRevision = deviceInformation.hardwareRevision
+ config.airpodsUpdaterIdentifier = deviceInformation.updaterIdentifier
+
+ val model = AirPodsModels.getModelByModelNumber(config.airpodsModelNumber)
+ if (model != null) {
+ airpodsInstance = AirPodsInstance(
+ name = config.airpodsName,
+ model = model,
+ actualModelNumber = config.airpodsModelNumber,
+ serialNumber = config.airpodsSerialNumber,
+ leftSerialNumber = config.airpodsLeftSerialNumber,
+ rightSerialNumber = config.airpodsRightSerialNumber,
+ version1 = config.airpodsVersion1,
+ version2 = config.airpodsVersion2,
+ version3 = config.airpodsVersion3,
+ aacpManager = aacpManager,
+ attManager = attManager
+ )
+ }
}
@SuppressLint("NewApi")
@@ -1167,6 +1226,19 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
rightLongPressAction = StemAction.fromString(sharedPreferences.getString("right_long_press_action", "DIGITAL_ASSISTANT") ?: "DIGITAL_ASSISTANT")!!,
cameraAction = sharedPreferences.getString("camera_action", null)?.let { AACPManager.Companion.StemPressType.valueOf(it) },
+
+ // AirPods device information
+ airpodsName = sharedPreferences.getString("airpods_name", "") ?: "",
+ airpodsModelNumber = sharedPreferences.getString("airpods_model_number", "") ?: "",
+ airpodsManufacturer = sharedPreferences.getString("airpods_manufacturer", "") ?: "",
+ airpodsSerialNumber = sharedPreferences.getString("airpods_serial_number", "") ?: "",
+ airpodsLeftSerialNumber = sharedPreferences.getString("airpods_left_serial_number", "") ?: "",
+ airpodsRightSerialNumber = sharedPreferences.getString("airpods_right_serial_number", "") ?: "",
+ airpodsVersion1 = sharedPreferences.getString("airpods_version1", "") ?: "",
+ airpodsVersion2 = sharedPreferences.getString("airpods_version2", "") ?: "",
+ airpodsVersion3 = sharedPreferences.getString("airpods_version3", "") ?: "",
+ airpodsHardwareRevision = sharedPreferences.getString("airpods_hardware_revision", "") ?: "",
+ airpodsUpdaterIdentifier = sharedPreferences.getString("airpods_updater_identifier", "") ?: "",
)
}
@@ -1248,6 +1320,19 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
setupStemActions()
}
"camera_action" -> config.cameraAction = preferences.getString(key, null)?.let { AACPManager.Companion.StemPressType.valueOf(it) }
+
+ // AirPods device information
+ "airpods_name" -> config.airpodsName = preferences.getString(key, "") ?: ""
+ "airpods_model_number" -> config.airpodsModelNumber = preferences.getString(key, "") ?: ""
+ "airpods_manufacturer" -> config.airpodsManufacturer = preferences.getString(key, "") ?: ""
+ "airpods_serial_number" -> config.airpodsSerialNumber = preferences.getString(key, "") ?: ""
+ "airpods_left_serial_number" -> config.airpodsLeftSerialNumber = preferences.getString(key, "") ?: ""
+ "airpods_right_serial_number" -> config.airpodsRightSerialNumber = preferences.getString(key, "") ?: ""
+ "airpods_version1" -> config.airpodsVersion1 = preferences.getString(key, "") ?: ""
+ "airpods_version2" -> config.airpodsVersion2 = preferences.getString(key, "") ?: ""
+ "airpods_version3" -> config.airpodsVersion3 = preferences.getString(key, "") ?: ""
+ "airpods_hardware_revision" -> config.airpodsHardwareRevision = preferences.getString(key, "") ?: ""
+ "airpods_updater_identifier" -> config.airpodsUpdaterIdentifier = preferences.getString(key, "") ?: ""
}
if (key == "mac_address") {
@@ -1747,7 +1832,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
updatedNotification = NotificationCompat.Builder(this, "background_service_status")
.setSmallIcon(R.drawable.airpods)
.setContentTitle("AirPods not connected")
- .setContentText("Tap to open app")
+ .setContentText("Tap to open app")
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW)
@@ -1968,15 +2053,17 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
private fun setMetadatas(d: BluetoothDevice) {
d.let{ device ->
- val metadataSet = SystemApisUtils.setMetadata(
+ val instance = airpodsInstance
+ if (instance != null) {
+ val metadataSet = SystemApisUtils.setMetadata(
device,
device.METADATA_MAIN_ICON,
- resToUri(R.drawable.pro_2).toString().toByteArray()
+ resToUri(instance.model.budCaseRes).toString().toByteArray()
) &&
SystemApisUtils.setMetadata(
device,
device.METADATA_MODEL_NAME,
- "AirPods Pro (2 Gen.)".toByteArray()
+ instance.model.name.toByteArray()
) &&
SystemApisUtils.setMetadata(
device,
@@ -1986,22 +2073,22 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
SystemApisUtils.setMetadata(
device,
device.METADATA_UNTETHERED_CASE_ICON,
- resToUri(R.drawable.pro_2_case).toString().toByteArray()
+ resToUri(instance.model.caseRes).toString().toByteArray()
) &&
SystemApisUtils.setMetadata(
device,
device.METADATA_UNTETHERED_RIGHT_ICON,
- resToUri(R.drawable.pro_2_right).toString().toByteArray()
+ resToUri(instance.model.rightBudsRes).toString().toByteArray()
) &&
SystemApisUtils.setMetadata(
device,
device.METADATA_UNTETHERED_LEFT_ICON,
- resToUri(R.drawable.pro_2_left).toString().toByteArray()
+ resToUri(instance.model.leftBudsRes).toString().toByteArray()
) &&
SystemApisUtils.setMetadata(
device,
device.METADATA_MANUFACTURER_NAME,
- "Apple".toByteArray()
+ instance.model.manufacturer.toByteArray()
) &&
SystemApisUtils.setMetadata(
device,
@@ -2023,7 +2110,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
device.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD,
"20".toByteArray()
)
- Log.d(TAG, "Metadata set: $metadataSet")
+ Log.d(TAG, "Metadata set: $metadataSet")
+ } else {
+ Log.w(TAG, "AirPods instance is not of type AirPodsInstance, skipping metadata setting")
+ }
}
}
@@ -2047,6 +2137,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
Log.d(TAG, "Received bluetooth connection broadcast")
if (ServiceManager.getService()?.isConnectedLocally == true) {
+ Log.d(TAG, "Device is already connected locally, ignoring broadcast")
ServiceManager.getService()?.manuallyCheckForAudioSource()
return
}
@@ -2088,6 +2179,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
fun manuallyCheckForAudioSource() {
val shouldResume = MediaController.getMusicActive()
+ if (airpodsInstance == null) return
if ((earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) || disconnectedBecauseReversed || otherDeviceTookOver) {
Log.d(
TAG,
@@ -2313,6 +2405,26 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
attManager = ATTManager(device)
attManager!!.connect()
+ // Create AirPodsInstance from stored config if available
+ if (airpodsInstance == null && config.airpodsModelNumber.isNotEmpty()) {
+ val model = AirPodsModels.getModelByModelNumber(config.airpodsModelNumber)
+ if (model != null) {
+ airpodsInstance = AirPodsInstance(
+ name = config.airpodsName,
+ model = model,
+ actualModelNumber = config.airpodsModelNumber,
+ serialNumber = config.airpodsSerialNumber,
+ leftSerialNumber = config.airpodsLeftSerialNumber,
+ rightSerialNumber = config.airpodsRightSerialNumber,
+ version1 = config.airpodsVersion1,
+ version2 = config.airpodsVersion2,
+ version3 = config.airpodsVersion3,
+ aacpManager = aacpManager,
+ attManager = attManager
+ )
+ }
+ }
+
updateNotificationContent(
true,
config.deviceName,
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt
index cc2a965..cd5392d 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt
@@ -118,7 +118,9 @@ class AACPManager {
ALLOW_AUTO_CONNECT(0x36), // not sure what this does, AUTOMATIC_CONNECTION is the only one used, but this is newer... so ¯\_(ツ)_/¯
EAR_DETECTION_CONFIG(0x0A),
AUTOMATIC_CONNECTION_CONFIG(0x20),
- OWNS_CONNECTION(0x06);
+ OWNS_CONNECTION(0x06),
+ PPE_TOGGLE_CONFIG(0x37),
+ PPE_CAP_LEVEL_CONFIG(0x38);
companion object {
fun fromByte(byte: Byte): ControlCommandIdentifiers? =
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/AirPods.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/AirPods.kt
new file mode 100644
index 0000000..4128130
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/AirPods.kt
@@ -0,0 +1,232 @@
+/*
+ * LibrePods - AirPods liberated from Apple’s ecosystem
+ *
+ * Copyright (C) 2025 LibrePods contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package me.kavishdevar.librepods.utils
+
+import me.kavishdevar.librepods.utils.AACPManager
+import me.kavishdevar.librepods.utils.ATTManager
+import me.kavishdevar.librepods.R
+
+open class AirPodsBase(
+ val modelNumber: List,
+ val name: String,
+ val displayName: String = "AirPods",
+ val manufacturer: String = "Apple Inc.",
+ val budCaseRes: Int,
+ val budsRes: Int,
+ val leftBudsRes: Int,
+ val rightBudsRes: Int,
+ val caseRes: Int,
+ val capabilities: Set
+)
+enum class Capability {
+ LISTENING_MODE,
+ CONVERSATION_AWARENESS,
+ STEM_CONFIG,
+ HEAD_GESTURES,
+ LOUD_SOUND_REDUCTION,
+ PPE,
+ SLEEP_DETECTION,
+ HEARING_AID,
+ ADAPTIVE_AUDIO,
+ ADAPTIVE_VOLUME,
+ SWIPE_FOR_VOLUME,
+ HRM
+}
+
+class AirPods: AirPodsBase(
+ modelNumber = listOf("A1523", "A1722"),
+ name = "AirPods 1",
+ budCaseRes = R.drawable.airpods_1,
+ budsRes = R.drawable.airpods_1_buds,
+ leftBudsRes = R.drawable.airpods_1_left,
+ rightBudsRes = R.drawable.airpods_1_right,
+ caseRes = R.drawable.airpods_1_case,
+ capabilities = emptySet()
+)
+
+class AirPods2: AirPodsBase(
+ modelNumber = listOf("A2032", "A2031"),
+ name = "AirPods 2",
+ budCaseRes = R.drawable.airpods_2,
+ budsRes = R.drawable.airpods_2_buds,
+ leftBudsRes = R.drawable.airpods_2_left,
+ rightBudsRes = R.drawable.airpods_2_right,
+ caseRes = R.drawable.airpods_2_case,
+ capabilities = emptySet()
+)
+
+class AirPods3: AirPodsBase(
+ modelNumber = listOf("A2565", "A2564"),
+ name = "AirPods 3",
+ budCaseRes = R.drawable.airpods_3,
+ budsRes = R.drawable.airpods_3_buds,
+ leftBudsRes = R.drawable.airpods_3_left,
+ rightBudsRes = R.drawable.airpods_3_right,
+ caseRes = R.drawable.airpods_3_case,
+ capabilities = setOf(
+ Capability.HEAD_GESTURES
+ )
+)
+
+class AirPods4: AirPodsBase(
+ modelNumber = listOf("A3053", "A3050", "A3054"),
+ name = "AirPods 4",
+ budCaseRes = R.drawable.airpods_4,
+ budsRes = R.drawable.airpods_4_buds,
+ leftBudsRes = R.drawable.airpods_4_left,
+ rightBudsRes = R.drawable.airpods_4_right,
+ caseRes = R.drawable.airpods_4_case,
+ capabilities = setOf(
+ Capability.HEAD_GESTURES,
+ Capability.SLEEP_DETECTION,
+ Capability.ADAPTIVE_VOLUME
+ )
+)
+
+class AirPods4ANC: AirPodsBase(
+ modelNumber = listOf("A3056", "A3055", "A3057"),
+ name = "AirPods 4 (ANC)",
+ budCaseRes = R.drawable.airpods_4,
+ budsRes = R.drawable.airpods_4_buds,
+ leftBudsRes = R.drawable.airpods_4_left,
+ rightBudsRes = R.drawable.airpods_4_right,
+ caseRes = R.drawable.airpods_4_case,
+ capabilities = setOf(
+ Capability.LISTENING_MODE,
+ Capability.CONVERSATION_AWARENESS,
+ Capability.HEAD_GESTURES,
+ Capability.ADAPTIVE_AUDIO,
+ Capability.SLEEP_DETECTION,
+ Capability.ADAPTIVE_VOLUME
+ )
+)
+
+class AirPodsPro1: AirPodsBase(
+ modelNumber = listOf("A2084", "A2083"),
+ name = "AirPods Pro 1",
+ displayName = "AirPods Pro",
+ budCaseRes = R.drawable.airpods_pro_1,
+ budsRes = R.drawable.airpods_pro_1_buds,
+ leftBudsRes = R.drawable.airpods_pro_1_left,
+ rightBudsRes = R.drawable.airpods_pro_1_right,
+ caseRes = R.drawable.airpods_pro_1_case,
+ capabilities = setOf(
+ Capability.LISTENING_MODE
+ )
+)
+
+class AirPodsPro2Lightning: AirPodsBase(
+ modelNumber = listOf("A2931", "A2699", "A2698"),
+ name = "AirPods Pro 2 with Magsafe Charging Case (Lightning)",
+ displayName = "AirPods Pro",
+ budCaseRes = R.drawable.airpods_pro_2,
+ budsRes = R.drawable.airpods_pro_2_buds,
+ leftBudsRes = R.drawable.airpods_pro_2_left,
+ rightBudsRes = R.drawable.airpods_pro_2_right,
+ caseRes = R.drawable.airpods_pro_2_case,
+ capabilities = setOf(
+ Capability.LISTENING_MODE,
+ Capability.CONVERSATION_AWARENESS,
+ Capability.STEM_CONFIG,
+ Capability.LOUD_SOUND_REDUCTION,
+ Capability.SLEEP_DETECTION,
+ Capability.HEARING_AID,
+ Capability.ADAPTIVE_AUDIO,
+ Capability.ADAPTIVE_VOLUME,
+ Capability.SWIPE_FOR_VOLUME
+ )
+)
+
+class AirPodsPro2USBC: AirPodsBase(
+ modelNumber = listOf("A3047", "A3048", "A3049"),
+ name = "AirPods Pro 2 with Magsafe Charging Case (USB-C)",
+ displayName = "AirPods Pro",
+ budCaseRes = R.drawable.airpods_pro_2,
+ budsRes = R.drawable.airpods_pro_2_buds,
+ leftBudsRes = R.drawable.airpods_pro_2_left,
+ rightBudsRes = R.drawable.airpods_pro_2_right,
+ caseRes = R.drawable.airpods_pro_2_case,
+ capabilities = setOf(
+ Capability.LISTENING_MODE,
+ Capability.CONVERSATION_AWARENESS,
+ Capability.STEM_CONFIG,
+ Capability.LOUD_SOUND_REDUCTION,
+ Capability.SLEEP_DETECTION,
+ Capability.HEARING_AID,
+ Capability.ADAPTIVE_AUDIO,
+ Capability.ADAPTIVE_VOLUME,
+ Capability.SWIPE_FOR_VOLUME
+ )
+)
+
+class AirPodsPro3: AirPodsBase(
+ modelNumber = listOf("A3063", "A3064", "A3065"),
+ name = "AirPods Pro 3",
+ displayName = "AirPods Pro",
+ budCaseRes = R.drawable.airpods_pro_3,
+ budsRes = R.drawable.airpods_pro_3_buds,
+ leftBudsRes = R.drawable.airpods_pro_3_left,
+ rightBudsRes = R.drawable.airpods_pro_3_right,
+ caseRes = R.drawable.airpods_pro_3_case,
+ capabilities = setOf(
+ Capability.LISTENING_MODE,
+ Capability.CONVERSATION_AWARENESS,
+ Capability.STEM_CONFIG,
+ Capability.LOUD_SOUND_REDUCTION,
+ Capability.PPE,
+ Capability.SLEEP_DETECTION,
+ Capability.HEARING_AID,
+ Capability.ADAPTIVE_AUDIO,
+ Capability.ADAPTIVE_VOLUME,
+ Capability.SWIPE_FOR_VOLUME,
+ Capability.HRM
+ )
+)
+
+data class AirPodsInstance(
+ val name: String,
+ val model: AirPodsBase,
+ val actualModelNumber: String,
+ val serialNumber: String?,
+ val leftSerialNumber: String?,
+ val rightSerialNumber: String?,
+ val version1: String?,
+ val version2: String?,
+ val version3: String?,
+ val aacpManager: AACPManager,
+ val attManager: ATTManager?
+)
+
+object AirPodsModels {
+ val models: List = listOf(
+ AirPods(),
+ AirPods2(),
+ AirPods3(),
+ AirPods4(),
+ AirPods4ANC(),
+ AirPodsPro1(),
+ AirPodsPro2Lightning(),
+ AirPodsPro2USBC(),
+ AirPodsPro3()
+ )
+
+ fun getModelByModelNumber(modelNumber: String): AirPodsBase? {
+ return models.find { modelNumber in it.modelNumber }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/pro_2.png b/android/app/src/main/res-apple/drawable/airpods_1.png
similarity index 100%
rename from android/app/src/main/res/drawable/pro_2.png
rename to android/app/src/main/res-apple/drawable/airpods_1.png
diff --git a/android/app/src/main/res/drawable/pro_2_buds.png b/android/app/src/main/res-apple/drawable/airpods_1_buds.png
similarity index 100%
rename from android/app/src/main/res/drawable/pro_2_buds.png
rename to android/app/src/main/res-apple/drawable/airpods_1_buds.png
diff --git a/android/app/src/main/res/drawable/pro_2_case.png b/android/app/src/main/res-apple/drawable/airpods_1_case.png
similarity index 100%
rename from android/app/src/main/res/drawable/pro_2_case.png
rename to android/app/src/main/res-apple/drawable/airpods_1_case.png
diff --git a/android/app/src/main/res/drawable/pro_2_left.png b/android/app/src/main/res-apple/drawable/airpods_1_left.png
similarity index 100%
rename from android/app/src/main/res/drawable/pro_2_left.png
rename to android/app/src/main/res-apple/drawable/airpods_1_left.png
diff --git a/android/app/src/main/res/drawable/pro_2_right.png b/android/app/src/main/res-apple/drawable/airpods_1_right.png
similarity index 100%
rename from android/app/src/main/res/drawable/pro_2_right.png
rename to android/app/src/main/res-apple/drawable/airpods_1_right.png
diff --git a/android/app/src/main/res-apple/drawable/airpods_2.png b/android/app/src/main/res-apple/drawable/airpods_2.png
new file mode 100644
index 0000000..681ee75
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_2.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_2_buds.png b/android/app/src/main/res-apple/drawable/airpods_2_buds.png
new file mode 100644
index 0000000..8bea6a2
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_2_buds.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_2_case.png b/android/app/src/main/res-apple/drawable/airpods_2_case.png
new file mode 100644
index 0000000..d904a2f
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_2_case.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_2_left.png b/android/app/src/main/res-apple/drawable/airpods_2_left.png
new file mode 100644
index 0000000..88e1394
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_2_left.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_2_right.png b/android/app/src/main/res-apple/drawable/airpods_2_right.png
new file mode 100644
index 0000000..76495be
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_2_right.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_3.png b/android/app/src/main/res-apple/drawable/airpods_3.png
new file mode 100644
index 0000000..681ee75
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_3.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_3_buds.png b/android/app/src/main/res-apple/drawable/airpods_3_buds.png
new file mode 100644
index 0000000..8bea6a2
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_3_buds.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_3_case.png b/android/app/src/main/res-apple/drawable/airpods_3_case.png
new file mode 100644
index 0000000..d904a2f
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_3_case.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_3_left.png b/android/app/src/main/res-apple/drawable/airpods_3_left.png
new file mode 100644
index 0000000..88e1394
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_3_left.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_3_right.png b/android/app/src/main/res-apple/drawable/airpods_3_right.png
new file mode 100644
index 0000000..76495be
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_3_right.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_4.png b/android/app/src/main/res-apple/drawable/airpods_4.png
new file mode 100644
index 0000000..681ee75
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_4.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_4_buds.png b/android/app/src/main/res-apple/drawable/airpods_4_buds.png
new file mode 100644
index 0000000..8bea6a2
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_4_buds.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_4_case.png b/android/app/src/main/res-apple/drawable/airpods_4_case.png
new file mode 100644
index 0000000..d904a2f
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_4_case.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_4_left.png b/android/app/src/main/res-apple/drawable/airpods_4_left.png
new file mode 100644
index 0000000..88e1394
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_4_left.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_4_right.png b/android/app/src/main/res-apple/drawable/airpods_4_right.png
new file mode 100644
index 0000000..76495be
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_4_right.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_1.png b/android/app/src/main/res-apple/drawable/airpods_pro_1.png
new file mode 100644
index 0000000..681ee75
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_1.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_1_buds.png b/android/app/src/main/res-apple/drawable/airpods_pro_1_buds.png
new file mode 100644
index 0000000..8bea6a2
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_1_buds.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_1_case.png b/android/app/src/main/res-apple/drawable/airpods_pro_1_case.png
new file mode 100644
index 0000000..d904a2f
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_1_case.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_1_left.png b/android/app/src/main/res-apple/drawable/airpods_pro_1_left.png
new file mode 100644
index 0000000..88e1394
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_1_left.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_1_right.png b/android/app/src/main/res-apple/drawable/airpods_pro_1_right.png
new file mode 100644
index 0000000..76495be
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_1_right.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_2.png b/android/app/src/main/res-apple/drawable/airpods_pro_2.png
new file mode 100644
index 0000000..681ee75
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_2.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_2_buds.png b/android/app/src/main/res-apple/drawable/airpods_pro_2_buds.png
new file mode 100644
index 0000000..8bea6a2
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_2_buds.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_2_case.png b/android/app/src/main/res-apple/drawable/airpods_pro_2_case.png
new file mode 100644
index 0000000..d904a2f
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_2_case.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_2_left.png b/android/app/src/main/res-apple/drawable/airpods_pro_2_left.png
new file mode 100644
index 0000000..88e1394
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_2_left.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_2_right.png b/android/app/src/main/res-apple/drawable/airpods_pro_2_right.png
new file mode 100644
index 0000000..76495be
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_2_right.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_3.png b/android/app/src/main/res-apple/drawable/airpods_pro_3.png
new file mode 100644
index 0000000..681ee75
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_3.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_3_buds.png b/android/app/src/main/res-apple/drawable/airpods_pro_3_buds.png
new file mode 100644
index 0000000..8bea6a2
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_3_buds.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_3_case.png b/android/app/src/main/res-apple/drawable/airpods_pro_3_case.png
new file mode 100644
index 0000000..d904a2f
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_3_case.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_3_left.png b/android/app/src/main/res-apple/drawable/airpods_pro_3_left.png
new file mode 100644
index 0000000..88e1394
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_3_left.png differ
diff --git a/android/app/src/main/res-apple/drawable/airpods_pro_3_right.png b/android/app/src/main/res-apple/drawable/airpods_pro_3_right.png
new file mode 100644
index 0000000..76495be
Binary files /dev/null and b/android/app/src/main/res-apple/drawable/airpods_pro_3_right.png differ
diff --git a/android/app/src/main/res/font/sf_pro.otf b/android/app/src/main/res-apple/font/sf_pro.otf
similarity index 100%
rename from android/app/src/main/res/font/sf_pro.otf
rename to android/app/src/main/res-apple/font/sf_pro.otf
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index e4c0766..a3da73f 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -82,7 +82,7 @@
Your phone starts playing media
Undo
You can customize Transparency mode for your AirPods Pro to help you hear what\'s around you.
- AirPods Pro automatically reduce your exposure to loud environmental noises when in Transparency and Adaptive mode.
+ Loud Sound Reduction can actively reduce your exposure to loud environmental noises when in Transparency and Adaptive mode. Loud Sound Reduction is not active in Off mode.
Loud Sound Reduction
Call Controls
Connect to this device automatically
@@ -193,4 +193,15 @@
Root access was denied. Please grant root permissions.
Troubleshooting Steps
Please enter the loss values in dbHL
+ About
+ Model Name
+ Model Number
+ Serial Number
+ Version
+ Hearing Health
+ Hearing Protection
+ Workspace Use
+ EN 352 Protection
+ EN 352 Protection limits the maximum level of media to 82 dBA, and meets applicable EN 352 Standard requirements for personal hearing protection.
+ Environmental Noise