android: add support for various models

still need to update images or find a way to fetch from apple's cdn
This commit is contained in:
Kavish Devar
2025-10-22 16:25:09 +05:30
parent 1a2f5138a9
commit 10fc96dc94
52 changed files with 1217 additions and 196 deletions

View File

@@ -44,6 +44,11 @@ android {
version = "3.22.1" version = "3.22.1"
} }
} }
sourceSets {
getByName("main") {
res.srcDirs("src/main/res", "src/main/res-apple")
}
}
} }
dependencies { dependencies {

View File

@@ -124,6 +124,7 @@ import me.kavishdevar.librepods.screens.DebugScreen
import me.kavishdevar.librepods.screens.HeadTrackingScreen import me.kavishdevar.librepods.screens.HeadTrackingScreen
import me.kavishdevar.librepods.screens.HearingAidAdjustmentsScreen import me.kavishdevar.librepods.screens.HearingAidAdjustmentsScreen
import me.kavishdevar.librepods.screens.HearingAidScreen import me.kavishdevar.librepods.screens.HearingAidScreen
import me.kavishdevar.librepods.screens.HearingProtectionScreen
import me.kavishdevar.librepods.screens.LongPress import me.kavishdevar.librepods.screens.LongPress
import me.kavishdevar.librepods.screens.Onboarding import me.kavishdevar.librepods.screens.Onboarding
import me.kavishdevar.librepods.screens.OpenSourceLicensesScreen 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.TransparencySettingsScreen
import me.kavishdevar.librepods.screens.TroubleshootingScreen import me.kavishdevar.librepods.screens.TroubleshootingScreen
import me.kavishdevar.librepods.screens.UpdateHearingTestScreen import me.kavishdevar.librepods.screens.UpdateHearingTestScreen
import me.kavishdevar.librepods.screens.VersionScreen
import me.kavishdevar.librepods.services.AirPodsService import me.kavishdevar.librepods.services.AirPodsService
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
import me.kavishdevar.librepods.utils.RadareOffsetFinder import me.kavishdevar.librepods.utils.RadareOffsetFinder
@@ -408,6 +410,12 @@ fun Main() {
composable("update_hearing_test") { composable("update_hearing_test") {
UpdateHearingTestScreen(navController) UpdateHearingTestScreen(navController)
} }
composable("version_info") {
VersionScreen(navController)
}
composable("hearing_protection") {
HearingProtectionScreen(navController)
}
} }
} }

View File

@@ -0,0 +1,205 @@
/*
* LibrePods - AirPods liberated from Apples 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 <https://www.gnu.org/licenses/>.
*/
@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
)
}
}

View File

@@ -42,15 +42,27 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.ATTHandles import me.kavishdevar.librepods.utils.ATTHandles
import me.kavishdevar.librepods.utils.Capability
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@Composable @Composable
fun AudioSettings(navController: NavController) { fun AudioSettings(navController: NavController) {
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black 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( Box(
modifier = Modifier modifier = Modifier
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)) .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
@@ -76,52 +88,60 @@ fun AudioSettings(navController: NavController) {
.padding(top = 2.dp) .padding(top = 2.dp)
) { ) {
StyledToggle( if (airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_VOLUME)) {
label = stringResource(R.string.personalized_volume), StyledToggle(
description = stringResource(R.string.personalized_volume_description), label = stringResource(R.string.personalized_volume),
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG, description = stringResource(R.string.personalized_volume_description),
independent = false controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
) independent = false
)
HorizontalDivider( HorizontalDivider(
thickness = 1.dp, thickness = 1.dp,
color = Color(0x40888888), color = Color(0x40888888),
modifier = Modifier modifier = Modifier
.padding(horizontal= 12.dp) .padding(horizontal = 12.dp)
) )
}
StyledToggle( if (airpodsInstance.model.capabilities.contains(Capability.CONVERSATION_AWARENESS)) {
label = stringResource(R.string.conversational_awareness), StyledToggle(
description = stringResource(R.string.conversational_awareness_description), label = stringResource(R.string.conversational_awareness),
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG, description = stringResource(R.string.conversational_awareness_description),
independent = false controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
) independent = false
HorizontalDivider( )
thickness = 1.dp, HorizontalDivider(
color = Color(0x40888888), thickness = 1.dp,
modifier = Modifier color = Color(0x40888888),
.padding(horizontal= 12.dp) modifier = Modifier
) .padding(horizontal = 12.dp)
)
}
StyledToggle( if (airpodsInstance.model.capabilities.contains(Capability.LOUD_SOUND_REDUCTION)){
label = stringResource(R.string.loud_sound_reduction), StyledToggle(
description = stringResource(R.string.loud_sound_reduction_description), label = stringResource(R.string.loud_sound_reduction),
attHandle = ATTHandles.LOUD_SOUND_REDUCTION, description = stringResource(R.string.loud_sound_reduction_description),
independent = false attHandle = ATTHandles.LOUD_SOUND_REDUCTION,
) independent = false
HorizontalDivider( )
thickness = 1.dp, HorizontalDivider(
color = Color(0x40888888), thickness = 1.dp,
modifier = Modifier color = Color(0x40888888),
.padding(horizontal= 12.dp) modifier = Modifier
) .padding(horizontal = 12.dp)
)
}
NavigationButton( if (airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_AUDIO)) {
to = "adaptive_strength", NavigationButton(
name = stringResource(R.string.adaptive_audio), to = "adaptive_strength",
navController = navController, name = stringResource(R.string.adaptive_audio),
independent = false navController = navController,
) independent = false
)
}
} }
} }

View File

@@ -135,6 +135,13 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
val singleDisplayed = remember { mutableStateOf(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 { Row {
Column ( Column (
modifier = Modifier modifier = Modifier
@@ -142,7 +149,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Image ( Image (
bitmap = ImageBitmap.imageResource(R.drawable.pro_2_buds), bitmap = ImageBitmap.imageResource(budsRes),
contentDescription = stringResource(R.string.buds), contentDescription = stringResource(R.string.buds),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -198,7 +205,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Image( Image(
bitmap = ImageBitmap.imageResource(R.drawable.pro_2_case), bitmap = ImageBitmap.imageResource(caseRes),
contentDescription = stringResource(R.string.case_alt), contentDescription = stringResource(R.string.case_alt),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@@ -61,7 +61,7 @@ fun ConnectionSettings() {
thickness = 1.dp, thickness = 1.dp,
color = Color(0x40888888), color = Color(0x40888888),
modifier = Modifier modifier = Modifier
.padding(horizontal= 12.dp) .padding(horizontal = 12.dp)
) )
StyledToggle( StyledToggle(

View File

@@ -0,0 +1,109 @@
/*
* LibrePods - AirPods liberated from Apples 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 <https://www.gnu.org/licenses/>.
*/
@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
)
}
}
}

View File

@@ -47,6 +47,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
@@ -59,7 +60,8 @@ fun NavigationButton(
independent: Boolean = true, independent: Boolean = true,
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
currentState: String? = null currentState: String? = null,
height: Dp = 58.dp,
) { ) {
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) } var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
@@ -84,7 +86,7 @@ fun NavigationButton(
Row( Row(
modifier = Modifier modifier = Modifier
.background(animatedBackgroundColor, RoundedCornerShape(if (independent) 28.dp else 0.dp)) .background(animatedBackgroundColor, RoundedCornerShape(if (independent) 28.dp else 0.dp))
.height(58.dp) .height(height)
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onPress = { onPress = {

View File

@@ -95,6 +95,7 @@ import me.kavishdevar.librepods.composables.StyledToggle
import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.ATTHandles import me.kavishdevar.librepods.utils.ATTHandles
import me.kavishdevar.librepods.utils.Capability
import me.kavishdevar.librepods.utils.RadareOffsetFinder import me.kavishdevar.librepods.utils.RadareOffsetFinder
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@@ -117,6 +118,8 @@ fun AccessibilitySettingsScreen(navController: NavController) {
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5) val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF) val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
val capabilities = remember { ServiceManager.getService()?.airpodsInstance?.model?.capabilities ?: emptySet<Capability>() }
val hearingAidEnabled = remember { mutableStateOf( 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_AID }?.value?.getOrNull(1) == 0x01.toByte() &&
aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }?.value?.getOrNull(0) == 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, independent = true,
) )
StyledToggle( if (capabilities.contains(Capability.LOUD_SOUND_REDUCTION)) {
label = stringResource(R.string.loud_sound_reduction), StyledToggle(
description = stringResource(R.string.loud_sound_reduction_description), label = stringResource(R.string.loud_sound_reduction),
attHandle = ATTHandles.LOUD_SOUND_REDUCTION description = stringResource(R.string.loud_sound_reduction_description),
) attHandle = ATTHandles.LOUD_SOUND_REDUCTION
)
}
if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) { if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) {
NavigationButton( NavigationButton(
@@ -399,29 +404,31 @@ fun AccessibilitySettingsScreen(navController: NavController) {
independent = true independent = true
) )
StyledToggle( if (capabilities.contains(Capability.SWIPE_FOR_VOLUME)) {
label = stringResource(R.string.volume_control), StyledToggle(
description = stringResource(R.string.volume_control_description), label = stringResource(R.string.volume_control),
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, description = stringResource(R.string.volume_control_description),
) controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE,
)
DropdownMenuComponent( DropdownMenuComponent(
label = stringResource(R.string.volume_swipe_speed), label = stringResource(R.string.volume_swipe_speed),
description = stringResource(R.string.volume_swipe_speed_description), description = stringResource(R.string.volume_swipe_speed_description),
options = volumeSwipeSpeedOptions.values.toList(), options = volumeSwipeSpeedOptions.values.toList(),
selectedOption = selectedVolumeSwipeSpeed?: "Default", selectedOption = selectedVolumeSwipeSpeed?: "Default",
onOptionSelected = { newValue -> onOptionSelected = { newValue ->
selectedVolumeSwipeSpeed = newValue selectedVolumeSwipeSpeed = newValue
aacpManager?.sendControlCommand( aacpManager?.sendControlCommand(
identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value, identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value,
value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull() value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
?: 1.toByte() ?: 1.toByte()
) )
}, },
textColor = textColor, textColor = textColor,
hazeState = hazeState, hazeState = hazeState,
independent = true independent = true
) )
}
if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) { if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) {
Text( Text(
@@ -562,88 +569,89 @@ fun AccessibilitySettingsScreen(navController: NavController) {
} }
} }
Column( // EQ Settings. Don't seem to have an effect?
modifier = Modifier // Column(
.fillMaxWidth() // modifier = Modifier
.background(backgroundColor, RoundedCornerShape(28.dp)) // .fillMaxWidth()
.padding(12.dp), // .background(backgroundColor, RoundedCornerShape(28.dp))
horizontalAlignment = Alignment.CenterHorizontally // .padding(12.dp),
) { // horizontalAlignment = Alignment.CenterHorizontally
for (i in 0 until 8) { // ) {
val eqPhoneValue = // for (i in 0 until 8) {
remember(phoneMediaEQ.value[i]) { mutableFloatStateOf(phoneMediaEQ.value[i]) } // val eqPhoneValue =
Row( // remember(phoneMediaEQ.value[i]) { mutableFloatStateOf(phoneMediaEQ.value[i]) }
horizontalArrangement = Arrangement.SpaceBetween, // Row(
verticalAlignment = Alignment.CenterVertically, // horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier // verticalAlignment = Alignment.CenterVertically,
.fillMaxWidth() // modifier = Modifier
.height(38.dp) // .fillMaxWidth()
) { // .height(38.dp)
Text( // ) {
text = String.format("%.2f", eqPhoneValue.floatValue), // Text(
fontSize = 12.sp, // text = String.format("%.2f", eqPhoneValue.floatValue),
color = textColor, // fontSize = 12.sp,
modifier = Modifier.padding(bottom = 4.dp) // color = textColor,
) // modifier = Modifier.padding(bottom = 4.dp)
// )
Slider( // Slider(
value = eqPhoneValue.floatValue, // value = eqPhoneValue.floatValue,
onValueChange = { newVal -> // onValueChange = { newVal ->
eqPhoneValue.floatValue = newVal // eqPhoneValue.floatValue = newVal
val newEQ = phoneMediaEQ.value.copyOf() // val newEQ = phoneMediaEQ.value.copyOf()
newEQ[i] = eqPhoneValue.floatValue // newEQ[i] = eqPhoneValue.floatValue
phoneMediaEQ.value = newEQ // phoneMediaEQ.value = newEQ
}, // },
valueRange = 0f..100f, // valueRange = 0f..100f,
modifier = Modifier // modifier = Modifier
.fillMaxWidth(0.9f) // .fillMaxWidth(0.9f)
.height(36.dp), // .height(36.dp),
colors = SliderDefaults.colors( // colors = SliderDefaults.colors(
thumbColor = thumbColor, // thumbColor = thumbColor,
activeTrackColor = activeTrackColor, // activeTrackColor = activeTrackColor,
inactiveTrackColor = trackColor // inactiveTrackColor = trackColor
), // ),
thumb = { // thumb = {
Box( // Box(
modifier = Modifier // modifier = Modifier
.size(24.dp) // .size(24.dp)
.shadow(4.dp, CircleShape) // .shadow(4.dp, CircleShape)
.background(thumbColor, CircleShape) // .background(thumbColor, CircleShape)
) // )
}, // },
track = { // track = {
Box( // Box(
modifier = Modifier // modifier = Modifier
.fillMaxWidth() // .fillMaxWidth()
.height(12.dp), // .height(12.dp),
contentAlignment = Alignment.CenterStart // contentAlignment = Alignment.CenterStart
) // )
{ // {
Box( // Box(
modifier = Modifier // modifier = Modifier
.fillMaxWidth() // .fillMaxWidth()
.height(4.dp) // .height(4.dp)
.background(trackColor, RoundedCornerShape(4.dp)) // .background(trackColor, RoundedCornerShape(4.dp))
) // )
Box( // Box(
modifier = Modifier // modifier = Modifier
.fillMaxWidth(eqPhoneValue.floatValue / 100f) // .fillMaxWidth(eqPhoneValue.floatValue / 100f)
.height(4.dp) // .height(4.dp)
.background(activeTrackColor, RoundedCornerShape(4.dp)) // .background(activeTrackColor, RoundedCornerShape(4.dp))
) // )
} // }
} // }
) // )
Text( // Text(
text = stringResource(R.string.band_label, i + 1), // text = stringResource(R.string.band_label, i + 1),
fontSize = 12.sp, // fontSize = 12.sp,
color = textColor, // color = textColor,
modifier = Modifier.padding(top = 4.dp) // modifier = Modifier.padding(top = 4.dp)
) // )
} // }
} // }
} // }
} }
} }
} }

View File

@@ -75,10 +75,12 @@ import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.composables.AboutCard
import me.kavishdevar.librepods.composables.AudioSettings import me.kavishdevar.librepods.composables.AudioSettings
import me.kavishdevar.librepods.composables.BatteryView import me.kavishdevar.librepods.composables.BatteryView
import me.kavishdevar.librepods.composables.CallControlSettings import me.kavishdevar.librepods.composables.CallControlSettings
import me.kavishdevar.librepods.composables.ConnectionSettings import me.kavishdevar.librepods.composables.ConnectionSettings
import me.kavishdevar.librepods.composables.HearingHealthSettings
import me.kavishdevar.librepods.composables.MicrophoneSettings import me.kavishdevar.librepods.composables.MicrophoneSettings
import me.kavishdevar.librepods.composables.NavigationButton import me.kavishdevar.librepods.composables.NavigationButton
import me.kavishdevar.librepods.composables.NoiseControlSettings 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.services.AirPodsService
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.Capability
import me.kavishdevar.librepods.utils.RadareOffsetFinder import me.kavishdevar.librepods.utils.RadareOffsetFinder
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@@ -225,6 +228,12 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
snackbarHostState = snackbarHostState snackbarHostState = snackbarHostState
) { spacerHeight, hazeState -> ) { spacerHeight, hazeState ->
if (isLocallyConnected || isRemotelyConnected) { if (isLocallyConnected || isRemotelyConnected) {
val instance = service.airpodsInstance
if (instance == null) {
Text("Error: AirPods instance is null")
return@StyledScaffold
}
val capabilities = instance.model.capabilities
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -248,27 +257,29 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
} }
val actAsAppleDeviceHookEnabled = RadareOffsetFinder.isSdpOffsetAvailable() val actAsAppleDeviceHookEnabled = RadareOffsetFinder.isSdpOffsetAvailable()
if (actAsAppleDeviceHookEnabled) { if (actAsAppleDeviceHookEnabled) {
item(key = "spacer_hearing_aid") { Spacer(modifier = Modifier.height(32.dp)) } item(key = "spacer_hearing_health") { Spacer(modifier = Modifier.height(32.dp)) }
item(key = "hearing_aid") { item(key = "hearing_health") {
NavigationButton( HearingHealthSettings(navController = navController)
to = "hearing_aid",
name = stringResource(R.string.hearing_aid),
navController = navController
)
} }
} }
item(key = "spacer_noise") { Spacer(modifier = Modifier.height(16.dp)) } if (capabilities.contains(Capability.LISTENING_MODE)) {
item(key = "noise_control") { NoiseControlSettings(service = service) } 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.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 = "spacer_call") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "call_control") { CallControlSettings(hazeState = hazeState) } item(key = "call_control") { CallControlSettings(hazeState = hazeState) }
item(key = "spacer_camera") { Spacer(modifier = Modifier.height(16.dp)) } if (capabilities.contains(Capability.STEM_CONFIG)) {
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_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 = "spacer_audio") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "audio") { AudioSettings(navController = navController) } 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 = "spacer_microphone") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "microphone") { MicrophoneSettings(hazeState) } item(key = "microphone") { MicrophoneSettings(hazeState) }
item(key = "spacer_sleep") { Spacer(modifier = Modifier.height(16.dp)) } if (capabilities.contains(Capability.SLEEP_DETECTION)) {
item(key = "sleep_detection") { item(key = "spacer_sleep") { Spacer(modifier = Modifier.height(16.dp)) }
StyledToggle( item(key = "sleep_detection") {
label = stringResource(R.string.sleep_detection), StyledToggle(
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG label = stringResource(R.string.sleep_detection),
) controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG
)
}
} }
item(key = "spacer_head_tracking") { Spacer(modifier = Modifier.height(16.dp)) } if (capabilities.contains(Capability.HEAD_GESTURES)) {
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_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 = "spacer_accessibility") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "accessibility") { NavigationButton(to = "accessibility", name = stringResource(R.string.accessibility), navController = navController) } item(key = "accessibility") { NavigationButton(to = "accessibility", name = stringResource(R.string.accessibility), navController = navController) }
item(key = "spacer_off_listening") { Spacer(modifier = Modifier.height(16.dp)) } if (capabilities.contains(Capability.LOUD_SOUND_REDUCTION)){
item(key = "off_listening") { item(key = "spacer_off_listening") { Spacer(modifier = Modifier.height(16.dp)) }
StyledToggle( item(key = "off_listening") {
label = stringResource(R.string.off_listening_mode), StyledToggle(
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION, label = stringResource(R.string.off_listening_mode),
description = stringResource(R.string.off_listening_mode_description) 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 = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "debug") { NavigationButton("debug", "Debug", navController) } item(key = "debug") { NavigationButton("debug", "Debug", navController) }

View File

@@ -0,0 +1,90 @@
/*
* LibrePods - AirPods liberated from Apples 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 <https://www.gnu.org/licenses/>.
*/
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
)
}
}
}

View File

@@ -0,0 +1,192 @@
/*
* LibrePods - AirPods liberated from Apples 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 <https://www.gnu.org/licenses/>.
*/
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))
)
)
}
}
}
}
}

View File

@@ -88,6 +88,8 @@ import me.kavishdevar.librepods.constants.StemAction
import me.kavishdevar.librepods.constants.isHeadTrackingData import me.kavishdevar.librepods.constants.isHeadTrackingData
import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType 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.ATTManager
import me.kavishdevar.librepods.utils.BLEManager import me.kavishdevar.librepods.utils.BLEManager
import me.kavishdevar.librepods.utils.BluetoothConnectionManager import me.kavishdevar.librepods.utils.BluetoothConnectionManager
@@ -152,6 +154,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var localMac = "" var localMac = ""
lateinit var aacpManager: AACPManager lateinit var aacpManager: AACPManager
var attManager: ATTManager? = null var attManager: ATTManager? = null
var airpodsInstance: AirPodsInstance? = null
var cameraActive = false var cameraActive = false
private var disconnectedBecauseReversed = false private var disconnectedBecauseReversed = false
private var otherDeviceTookOver = false private var otherDeviceTookOver = false
@@ -191,6 +194,19 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var rightLongPressAction: StemAction = StemAction.defaultActions[StemPressType.LONG_PRESS]!!, var rightLongPressAction: StemAction = StemAction.defaultActions[StemPressType.LONG_PRESS]!!,
var cameraAction: AACPManager.Companion.StemPressType? = null, 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 private lateinit var config: ServiceConfig
@@ -931,6 +947,49 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
"AirPodsParser", "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}" "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") @SuppressLint("NewApi")
@@ -1167,6 +1226,19 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
rightLongPressAction = StemAction.fromString(sharedPreferences.getString("right_long_press_action", "DIGITAL_ASSISTANT") ?: "DIGITAL_ASSISTANT")!!, 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) }, 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() setupStemActions()
} }
"camera_action" -> config.cameraAction = preferences.getString(key, null)?.let { AACPManager.Companion.StemPressType.valueOf(it) } "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") { if (key == "mac_address") {
@@ -1747,7 +1832,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
updatedNotification = NotificationCompat.Builder(this, "background_service_status") updatedNotification = NotificationCompat.Builder(this, "background_service_status")
.setSmallIcon(R.drawable.airpods) .setSmallIcon(R.drawable.airpods)
.setContentTitle("AirPods not connected") .setContentTitle("AirPods not connected")
.setContentText("Tap to open app") .setContentText("Tap to open app")
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SERVICE) .setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
@@ -1968,15 +2053,17 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
private fun setMetadatas(d: BluetoothDevice) { private fun setMetadatas(d: BluetoothDevice) {
d.let{ device -> d.let{ device ->
val metadataSet = SystemApisUtils.setMetadata( val instance = airpodsInstance
if (instance != null) {
val metadataSet = SystemApisUtils.setMetadata(
device, device,
device.METADATA_MAIN_ICON, device.METADATA_MAIN_ICON,
resToUri(R.drawable.pro_2).toString().toByteArray() resToUri(instance.model.budCaseRes).toString().toByteArray()
) && ) &&
SystemApisUtils.setMetadata( SystemApisUtils.setMetadata(
device, device,
device.METADATA_MODEL_NAME, device.METADATA_MODEL_NAME,
"AirPods Pro (2 Gen.)".toByteArray() instance.model.name.toByteArray()
) && ) &&
SystemApisUtils.setMetadata( SystemApisUtils.setMetadata(
device, device,
@@ -1986,22 +2073,22 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
SystemApisUtils.setMetadata( SystemApisUtils.setMetadata(
device, device,
device.METADATA_UNTETHERED_CASE_ICON, device.METADATA_UNTETHERED_CASE_ICON,
resToUri(R.drawable.pro_2_case).toString().toByteArray() resToUri(instance.model.caseRes).toString().toByteArray()
) && ) &&
SystemApisUtils.setMetadata( SystemApisUtils.setMetadata(
device, device,
device.METADATA_UNTETHERED_RIGHT_ICON, device.METADATA_UNTETHERED_RIGHT_ICON,
resToUri(R.drawable.pro_2_right).toString().toByteArray() resToUri(instance.model.rightBudsRes).toString().toByteArray()
) && ) &&
SystemApisUtils.setMetadata( SystemApisUtils.setMetadata(
device, device,
device.METADATA_UNTETHERED_LEFT_ICON, device.METADATA_UNTETHERED_LEFT_ICON,
resToUri(R.drawable.pro_2_left).toString().toByteArray() resToUri(instance.model.leftBudsRes).toString().toByteArray()
) && ) &&
SystemApisUtils.setMetadata( SystemApisUtils.setMetadata(
device, device,
device.METADATA_MANUFACTURER_NAME, device.METADATA_MANUFACTURER_NAME,
"Apple".toByteArray() instance.model.manufacturer.toByteArray()
) && ) &&
SystemApisUtils.setMetadata( SystemApisUtils.setMetadata(
device, device,
@@ -2023,7 +2110,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
device.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD, device.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD,
"20".toByteArray() "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()) { if (bluetoothDevice != null && action != null && !action.isEmpty()) {
Log.d(TAG, "Received bluetooth connection broadcast") Log.d(TAG, "Received bluetooth connection broadcast")
if (ServiceManager.getService()?.isConnectedLocally == true) { if (ServiceManager.getService()?.isConnectedLocally == true) {
Log.d(TAG, "Device is already connected locally, ignoring broadcast")
ServiceManager.getService()?.manuallyCheckForAudioSource() ServiceManager.getService()?.manuallyCheckForAudioSource()
return return
} }
@@ -2088,6 +2179,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
fun manuallyCheckForAudioSource() { fun manuallyCheckForAudioSource() {
val shouldResume = MediaController.getMusicActive() val shouldResume = MediaController.getMusicActive()
if (airpodsInstance == null) return
if ((earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) || disconnectedBecauseReversed || otherDeviceTookOver) { if ((earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) || disconnectedBecauseReversed || otherDeviceTookOver) {
Log.d( Log.d(
TAG, TAG,
@@ -2313,6 +2405,26 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
attManager = ATTManager(device) attManager = ATTManager(device)
attManager!!.connect() 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( updateNotificationContent(
true, true,
config.deviceName, config.deviceName,

View File

@@ -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 ¯\_(ツ)_/¯ 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), EAR_DETECTION_CONFIG(0x0A),
AUTOMATIC_CONNECTION_CONFIG(0x20), AUTOMATIC_CONNECTION_CONFIG(0x20),
OWNS_CONNECTION(0x06); OWNS_CONNECTION(0x06),
PPE_TOGGLE_CONFIG(0x37),
PPE_CAP_LEVEL_CONFIG(0x38);
companion object { companion object {
fun fromByte(byte: Byte): ControlCommandIdentifiers? = fun fromByte(byte: Byte): ControlCommandIdentifiers? =

View File

@@ -0,0 +1,232 @@
/*
* LibrePods - AirPods liberated from Apples 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 <https://www.gnu.org/licenses/>.
*/
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<String>,
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<Capability>
)
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<AirPodsBase> = listOf(
AirPods(),
AirPods2(),
AirPods3(),
AirPods4(),
AirPods4ANC(),
AirPodsPro1(),
AirPodsPro2Lightning(),
AirPodsPro2USBC(),
AirPodsPro3()
)
fun getModelByModelNumber(modelNumber: String): AirPodsBase? {
return models.find { modelNumber in it.modelNumber }
}
}

View File

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

Before

Width:  |  Height:  |  Size: 605 KiB

After

Width:  |  Height:  |  Size: 605 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -82,7 +82,7 @@
<string name="takeover_media_start_desc">Your phone starts playing media</string> <string name="takeover_media_start_desc">Your phone starts playing media</string>
<string name="undo">Undo</string> <string name="undo">Undo</string>
<string name="customize_transparency_mode_description">You can customize Transparency mode for your AirPods Pro to help you hear what\'s around you.</string> <string name="customize_transparency_mode_description">You can customize Transparency mode for your AirPods Pro to help you hear what\'s around you.</string>
<string name="loud_sound_reduction_description">AirPods Pro automatically reduce your exposure to loud environmental noises when in Transparency and Adaptive mode.</string> <string name="loud_sound_reduction_description">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.</string>
<string name="loud_sound_reduction">Loud Sound Reduction</string> <string name="loud_sound_reduction">Loud Sound Reduction</string>
<string name="call_controls">Call Controls</string> <string name="call_controls">Call Controls</string>
<string name="automatically_connect">Connect to this device automatically</string> <string name="automatically_connect">Connect to this device automatically</string>
@@ -193,4 +193,15 @@
<string name="root_access_denied">Root access was denied. Please grant root permissions.</string> <string name="root_access_denied">Root access was denied. Please grant root permissions.</string>
<string name="troubleshooting_steps">Troubleshooting Steps</string> <string name="troubleshooting_steps">Troubleshooting Steps</string>
<string name="hearing_test_value_instruction">Please enter the loss values in dbHL</string> <string name="hearing_test_value_instruction">Please enter the loss values in dbHL</string>
<string name="about">About</string>
<string name="model_name">Model Name</string>
<string name="model_number">Model Number</string>
<string name="serial_number">Serial Number</string>
<string name="version">Version</string>
<string name="hearing_health">Hearing Health</string>
<string name="hearing_protection">Hearing Protection</string>
<string name="workspace_use">Workspace Use</string>
<string name="ppe">EN 352 Protection</string>
<string name="workspace_use_description">EN 352 Protection limits the maximum level of media to 82 dBA, and meets applicable EN 352 Standard requirements for personal hearing protection.</string>
<string name="environmental_noise">Environmental Noise</string>
</resources> </resources>