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"
|
version = "3.22.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") {
|
||||||
|
res.srcDirs("src/main/res", "src/main/res-apple")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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.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 = {
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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.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,
|
||||||
|
|||||||
@@ -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? =
|
||||||
|
|||||||
@@ -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="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>
|
||||||
|
|||||||