android: add support for various models
still need to update images or find a way to fetch from apple's cdn
@@ -44,6 +44,11 @@ android {
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
res.srcDirs("src/main/res", "src/main/res-apple")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -61,7 +61,7 @@ fun ConnectionSettings() {
|
||||
thickness = 1.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier
|
||||
.padding(horizontal= 12.dp)
|
||||
.padding(horizontal = 12.dp)
|
||||
)
|
||||
|
||||
StyledToggle(
|
||||
|
||||
@@ -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 <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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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<Capability>() }
|
||||
|
||||
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)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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 <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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <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))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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? =
|
||||
|
||||
@@ -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 <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 }
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 605 KiB After Width: | Height: | Size: 605 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_2.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_2_buds.png
Normal file
|
After Width: | Height: | Size: 605 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_2_case.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_2_left.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_2_right.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_3.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_3_buds.png
Normal file
|
After Width: | Height: | Size: 605 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_3_case.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_3_left.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_3_right.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_4.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_4_buds.png
Normal file
|
After Width: | Height: | Size: 605 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_4_case.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_4_left.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_4_right.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_1.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_1_buds.png
Normal file
|
After Width: | Height: | Size: 605 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_1_case.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_1_left.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_1_right.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_2.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_2_buds.png
Normal file
|
After Width: | Height: | Size: 605 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_2_case.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_2_left.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_2_right.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_3.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_3_buds.png
Normal file
|
After Width: | Height: | Size: 605 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_3_case.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_3_left.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
android/app/src/main/res-apple/drawable/airpods_pro_3_right.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
@@ -82,7 +82,7 @@
|
||||
<string name="takeover_media_start_desc">Your phone starts playing media</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="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="call_controls">Call Controls</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="troubleshooting_steps">Troubleshooting Steps</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>
|
||||
|
||||