Skip to content

Commit 10fc96d

Browse files
committed
android: add support for various models
still need to update images or find a way to fetch from apple's cdn
1 parent 1a2f513 commit 10fc96d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1219
-198
lines changed

android/app/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ android {
4444
version = "3.22.1"
4545
}
4646
}
47+
sourceSets {
48+
getByName("main") {
49+
res.srcDirs("src/main/res", "src/main/res-apple")
50+
}
51+
}
4752
}
4853

4954
dependencies {

android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,15 @@ import me.kavishdevar.librepods.screens.DebugScreen
124124
import me.kavishdevar.librepods.screens.HeadTrackingScreen
125125
import me.kavishdevar.librepods.screens.HearingAidAdjustmentsScreen
126126
import me.kavishdevar.librepods.screens.HearingAidScreen
127+
import me.kavishdevar.librepods.screens.HearingProtectionScreen
127128
import me.kavishdevar.librepods.screens.LongPress
128129
import me.kavishdevar.librepods.screens.Onboarding
129130
import me.kavishdevar.librepods.screens.OpenSourceLicensesScreen
130131
import me.kavishdevar.librepods.screens.RenameScreen
131132
import me.kavishdevar.librepods.screens.TransparencySettingsScreen
132133
import me.kavishdevar.librepods.screens.TroubleshootingScreen
133134
import me.kavishdevar.librepods.screens.UpdateHearingTestScreen
135+
import me.kavishdevar.librepods.screens.VersionScreen
134136
import me.kavishdevar.librepods.services.AirPodsService
135137
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
136138
import me.kavishdevar.librepods.utils.RadareOffsetFinder
@@ -408,6 +410,12 @@ fun Main() {
408410
composable("update_hearing_test") {
409411
UpdateHearingTestScreen(navController)
410412
}
413+
composable("version_info") {
414+
VersionScreen(navController)
415+
}
416+
composable("hearing_protection") {
417+
HearingProtectionScreen(navController)
418+
}
411419
}
412420
}
413421

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* LibrePods - AirPods liberated from Apple’s ecosystem
3+
*
4+
* Copyright (C) 2025 LibrePods contributors
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
@file:OptIn(ExperimentalEncodingApi::class)
20+
21+
package me.kavishdevar.librepods.composables
22+
23+
import androidx.compose.foundation.background
24+
import androidx.compose.foundation.clickable
25+
import androidx.compose.foundation.isSystemInDarkTheme
26+
import androidx.compose.foundation.interaction.MutableInteractionSource
27+
import androidx.compose.foundation.layout.Arrangement
28+
import androidx.compose.foundation.layout.Box
29+
import androidx.compose.foundation.layout.Column
30+
import androidx.compose.foundation.layout.Row
31+
import androidx.compose.foundation.layout.fillMaxWidth
32+
import androidx.compose.foundation.layout.padding
33+
import androidx.compose.foundation.shape.RoundedCornerShape
34+
import androidx.compose.material3.HorizontalDivider
35+
import androidx.compose.material3.Text
36+
import androidx.compose.runtime.Composable
37+
import androidx.compose.runtime.mutableStateOf
38+
import androidx.compose.runtime.remember
39+
import androidx.compose.ui.Modifier
40+
import androidx.compose.ui.draw.clip
41+
import androidx.compose.ui.graphics.Color
42+
import androidx.compose.ui.res.stringResource
43+
import androidx.compose.ui.layout.onGloballyPositioned
44+
import androidx.compose.ui.platform.LocalDensity
45+
import androidx.compose.ui.text.TextStyle
46+
import androidx.compose.ui.text.font.Font
47+
import androidx.compose.ui.text.font.FontFamily
48+
import androidx.compose.ui.text.font.FontWeight
49+
import androidx.compose.ui.unit.Dp
50+
import androidx.compose.ui.unit.dp
51+
import androidx.compose.ui.unit.sp
52+
import androidx.navigation.NavController
53+
import me.kavishdevar.librepods.R
54+
import me.kavishdevar.librepods.composables.NavigationButton
55+
import me.kavishdevar.librepods.services.ServiceManager
56+
import kotlin.io.encoding.ExperimentalEncodingApi
57+
58+
@Composable
59+
fun AboutCard(navController: NavController) {
60+
val isDarkTheme = isSystemInDarkTheme()
61+
val textColor = if (isDarkTheme) Color.White else Color.Black
62+
val service = ServiceManager.getService()
63+
if (service == null) return
64+
val airpodsInstance = service.airpodsInstance
65+
if (airpodsInstance == null) return
66+
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
67+
68+
Box(
69+
modifier = Modifier
70+
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
71+
.padding(horizontal = 16.dp, vertical = 4.dp)
72+
){
73+
Text(
74+
text = stringResource(R.string.about),
75+
style = TextStyle(
76+
fontSize = 14.sp,
77+
fontWeight = FontWeight.Bold,
78+
color = textColor.copy(alpha = 0.6f)
79+
)
80+
)
81+
}
82+
83+
val rowHeight = remember { mutableStateOf(0.dp) }
84+
val density = LocalDensity.current
85+
86+
Column(
87+
modifier = Modifier
88+
.clip(RoundedCornerShape(28.dp))
89+
.fillMaxWidth()
90+
.background(backgroundColor, RoundedCornerShape(28.dp))
91+
.padding(top = 2.dp)
92+
) {
93+
Row(
94+
modifier = Modifier
95+
.fillMaxWidth()
96+
.padding(16.dp)
97+
.onGloballyPositioned { coordinates ->
98+
rowHeight.value = with(density) { coordinates.size.height.toDp() }
99+
},
100+
horizontalArrangement = Arrangement.SpaceBetween,
101+
) {
102+
Text(
103+
text = stringResource(R.string.model_name),
104+
style = TextStyle(
105+
fontSize = 16.sp,
106+
color = textColor,
107+
fontFamily = FontFamily(Font(R.font.sf_pro))
108+
)
109+
)
110+
Text(
111+
text = airpodsInstance.model.displayName,
112+
style = TextStyle(
113+
fontSize = 16.sp,
114+
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
115+
fontFamily = FontFamily(Font(R.font.sf_pro))
116+
)
117+
)
118+
}
119+
HorizontalDivider(
120+
thickness = 1.dp,
121+
color = Color(0x40888888),
122+
modifier = Modifier
123+
.padding(horizontal = 12.dp)
124+
)
125+
Row(
126+
modifier = Modifier
127+
.fillMaxWidth()
128+
.padding(16.dp),
129+
horizontalArrangement = Arrangement.SpaceBetween,
130+
) {
131+
Text(
132+
text = stringResource(R.string.model_name),
133+
style = TextStyle(
134+
fontSize = 16.sp,
135+
color = textColor,
136+
fontFamily = FontFamily(Font(R.font.sf_pro))
137+
)
138+
)
139+
Text(
140+
text = airpodsInstance.actualModelNumber,
141+
style = TextStyle(
142+
fontSize = 16.sp,
143+
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
144+
fontFamily = FontFamily(Font(R.font.sf_pro))
145+
)
146+
)
147+
}
148+
HorizontalDivider(
149+
thickness = 1.dp,
150+
color = Color(0x40888888),
151+
modifier = Modifier
152+
.padding(horizontal = 12.dp)
153+
)
154+
val serialNumbers = listOf(
155+
airpodsInstance.serialNumber?: "",
156+
"􀀛 ${airpodsInstance.leftSerialNumber}",
157+
"􀀧 ${airpodsInstance.rightSerialNumber}"
158+
)
159+
val serialNumber = remember { mutableStateOf(0) }
160+
Row(
161+
modifier = Modifier
162+
.fillMaxWidth()
163+
.padding(16.dp),
164+
horizontalArrangement = Arrangement.SpaceBetween,
165+
) {
166+
Text(
167+
text = stringResource(R.string.serial_number),
168+
style = TextStyle(
169+
fontSize = 16.sp,
170+
color = textColor,
171+
fontFamily = FontFamily(Font(R.font.sf_pro))
172+
),
173+
)
174+
Text(
175+
text = serialNumbers[serialNumber.value],
176+
style = TextStyle(
177+
fontSize = 16.sp,
178+
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
179+
fontFamily = FontFamily(Font(R.font.sf_pro))
180+
),
181+
modifier = Modifier
182+
.clickable(
183+
interactionSource = remember { MutableInteractionSource() },
184+
indication = null
185+
) {
186+
serialNumber.value = (serialNumber.value + 1) % serialNumbers.size
187+
}
188+
)
189+
}
190+
HorizontalDivider(
191+
thickness = 1.dp,
192+
color = Color(0x40888888),
193+
modifier = Modifier
194+
.padding(horizontal = 12.dp)
195+
)
196+
NavigationButton(
197+
to = "version_info",
198+
navController = navController,
199+
name = stringResource(R.string.version),
200+
currentState = airpodsInstance.version3,
201+
independent = false,
202+
height = rowHeight.value + 32.dp
203+
)
204+
}
205+
}

android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,27 @@ import androidx.compose.ui.unit.sp
4242
import androidx.navigation.NavController
4343
import androidx.navigation.compose.rememberNavController
4444
import me.kavishdevar.librepods.R
45+
import me.kavishdevar.librepods.services.ServiceManager
4546
import me.kavishdevar.librepods.utils.AACPManager
4647
import me.kavishdevar.librepods.utils.ATTHandles
48+
import me.kavishdevar.librepods.utils.Capability
4749
import kotlin.io.encoding.ExperimentalEncodingApi
4850

4951
@Composable
5052
fun AudioSettings(navController: NavController) {
5153
val isDarkTheme = isSystemInDarkTheme()
5254
val textColor = if (isDarkTheme) Color.White else Color.Black
53-
55+
val service = ServiceManager.getService()
56+
if (service == null) return
57+
val airpodsInstance = service.airpodsInstance
58+
if (airpodsInstance == null) return
59+
if (!airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_VOLUME) &&
60+
!airpodsInstance.model.capabilities.contains(Capability.CONVERSATION_AWARENESS) &&
61+
!airpodsInstance.model.capabilities.contains(Capability.LOUD_SOUND_REDUCTION) &&
62+
!airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_AUDIO)
63+
) {
64+
return
65+
}
5466
Box(
5567
modifier = Modifier
5668
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
@@ -76,52 +88,60 @@ fun AudioSettings(navController: NavController) {
7688
.padding(top = 2.dp)
7789
) {
7890

79-
StyledToggle(
80-
label = stringResource(R.string.personalized_volume),
81-
description = stringResource(R.string.personalized_volume_description),
82-
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
83-
independent = false
84-
)
91+
if (airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_VOLUME)) {
92+
StyledToggle(
93+
label = stringResource(R.string.personalized_volume),
94+
description = stringResource(R.string.personalized_volume_description),
95+
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
96+
independent = false
97+
)
8598

86-
HorizontalDivider(
87-
thickness = 1.dp,
88-
color = Color(0x40888888),
89-
modifier = Modifier
90-
.padding(horizontal= 12.dp)
91-
)
99+
HorizontalDivider(
100+
thickness = 1.dp,
101+
color = Color(0x40888888),
102+
modifier = Modifier
103+
.padding(horizontal = 12.dp)
104+
)
105+
}
92106

93-
StyledToggle(
94-
label = stringResource(R.string.conversational_awareness),
95-
description = stringResource(R.string.conversational_awareness_description),
96-
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
97-
independent = false
98-
)
99-
HorizontalDivider(
100-
thickness = 1.dp,
101-
color = Color(0x40888888),
102-
modifier = Modifier
103-
.padding(horizontal= 12.dp)
104-
)
107+
if (airpodsInstance.model.capabilities.contains(Capability.CONVERSATION_AWARENESS)) {
108+
StyledToggle(
109+
label = stringResource(R.string.conversational_awareness),
110+
description = stringResource(R.string.conversational_awareness_description),
111+
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
112+
independent = false
113+
)
114+
HorizontalDivider(
115+
thickness = 1.dp,
116+
color = Color(0x40888888),
117+
modifier = Modifier
118+
.padding(horizontal = 12.dp)
119+
)
120+
}
105121

106-
StyledToggle(
107-
label = stringResource(R.string.loud_sound_reduction),
108-
description = stringResource(R.string.loud_sound_reduction_description),
109-
attHandle = ATTHandles.LOUD_SOUND_REDUCTION,
110-
independent = false
111-
)
112-
HorizontalDivider(
113-
thickness = 1.dp,
114-
color = Color(0x40888888),
115-
modifier = Modifier
116-
.padding(horizontal= 12.dp)
117-
)
122+
if (airpodsInstance.model.capabilities.contains(Capability.LOUD_SOUND_REDUCTION)){
123+
StyledToggle(
124+
label = stringResource(R.string.loud_sound_reduction),
125+
description = stringResource(R.string.loud_sound_reduction_description),
126+
attHandle = ATTHandles.LOUD_SOUND_REDUCTION,
127+
independent = false
128+
)
129+
HorizontalDivider(
130+
thickness = 1.dp,
131+
color = Color(0x40888888),
132+
modifier = Modifier
133+
.padding(horizontal = 12.dp)
134+
)
135+
}
118136

119-
NavigationButton(
120-
to = "adaptive_strength",
121-
name = stringResource(R.string.adaptive_audio),
122-
navController = navController,
123-
independent = false
124-
)
137+
if (airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_AUDIO)) {
138+
NavigationButton(
139+
to = "adaptive_strength",
140+
name = stringResource(R.string.adaptive_audio),
141+
navController = navController,
142+
independent = false
143+
)
144+
}
125145
}
126146
}
127147

0 commit comments

Comments
 (0)