diff --git a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
index 8de1b77d..6d8502cc 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
@@ -91,6 +91,9 @@ import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.statusBars
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
@@ -106,6 +109,7 @@ import androidx.core.net.toUri
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
+import me.kavishdevar.librepods.utils.popBackStackSafely
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.isGranted
@@ -424,6 +428,8 @@ fun Main() {
}
}
+ val statusBarPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
+
AnimatedVisibility(
visible = showBackButton.value,
enter = fadeIn(animationSpec = tween()) + scaleIn(initialScale = 0f, animationSpec = tween()),
@@ -432,11 +438,11 @@ fun Main() {
.align(Alignment.TopStart)
.padding(
start = 8.dp,
- top = (LocalWindowInfo.current.containerSize.width * 0.05f).dp
+ top = statusBarPadding
)
) {
StyledIconButton(
- onClick = { navController.popBackStack() },
+ onClick = { navController.popBackStackSafely() },
icon = "",
darkMode = isSystemInDarkTheme(),
backdrop = backButtonBackdrop
@@ -551,7 +557,7 @@ fun PermissionsScreen(
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = "Permission Required",
+ text = stringResource(R.string.permission_required_title),
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
@@ -579,8 +585,8 @@ fun PermissionsScreen(
Spacer(modifier = Modifier.height(32.dp))
PermissionCard(
- title = "Bluetooth Permissions",
- description = "Required to communicate with your AirPods",
+ title = stringResource(R.string.bluetooth_permissions),
+ description = stringResource(R.string.bluetooth_permissions_desc),
icon = ImageVector.vectorResource(id = R.drawable.ic_bluetooth),
isGranted = permissionState.permissions.filter {
it.permission.contains("BLUETOOTH")
@@ -591,8 +597,8 @@ fun PermissionsScreen(
)
PermissionCard(
- title = "Notification Permission",
- description = "To show battery status",
+ title = stringResource(R.string.notification_permission),
+ description = stringResource(R.string.notification_permission_desc),
icon = Icons.Default.Notifications,
isGranted = permissionState.permissions.find {
it.permission == "android.permission.POST_NOTIFICATIONS"
@@ -603,8 +609,8 @@ fun PermissionsScreen(
)
PermissionCard(
- title = "Phone Permissions",
- description = "For answering calls with Head Gestures",
+ title = stringResource(R.string.phone_permissions),
+ description = stringResource(R.string.phone_permissions_desc),
icon = Icons.Default.Phone,
isGranted = permissionState.permissions.filter {
it.permission.contains("PHONE") || it.permission.contains("CALLS")
@@ -615,8 +621,8 @@ fun PermissionsScreen(
)
PermissionCard(
- title = "Display Over Other Apps",
- description = "For popup animations when AirPods connect",
+ title = stringResource(R.string.display_over_other_apps),
+ description = stringResource(R.string.display_over_other_apps_desc),
icon = ImageVector.vectorResource(id = R.drawable.ic_layers),
isGranted = canDrawOverlays,
backgroundColor = backgroundColor,
@@ -637,7 +643,7 @@ fun PermissionsScreen(
shape = RoundedCornerShape(8.dp)
) {
Text(
- "Ask for regular permissions",
+ stringResource(R.string.ask_regular_permissions),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
@@ -668,7 +674,7 @@ fun PermissionsScreen(
shape = RoundedCornerShape(8.dp)
) {
Text(
- if (canDrawOverlays) "Overlay Permission Granted" else "Grant Overlay Permission",
+ if (canDrawOverlays) stringResource(R.string.overlay_permission_granted) else stringResource(R.string.grant_overlay_permission),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
@@ -700,7 +706,7 @@ fun PermissionsScreen(
shape = RoundedCornerShape(8.dp)
) {
Text(
- "Continue without overlay",
+ stringResource(R.string.continue_without_overlay),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt
index 85a95730..f5d546c6 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt
@@ -81,6 +81,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -597,7 +598,7 @@ fun NewControlCenterDialogContent(
Spacer(modifier = Modifier.height(8.dp))
Text(
- text = "Conversational\nAwareness",
+ text = stringResource(R.string.conversational_awareness).replace(" ", "\n"),
color = Color.White,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
@@ -613,7 +614,7 @@ fun NewControlCenterDialogContent(
} else {
Spacer(modifier = Modifier.weight(1f))
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
- Text("Loading...", color = textColor)
+ Text(stringResource(R.string.loading), color = textColor)
}
Spacer(modifier = Modifier.weight(1f))
}
@@ -629,11 +630,12 @@ private fun getModeIconRes(mode: NoiseControlMode): Int {
}
}
+@Composable
private fun getModeLabel(mode: NoiseControlMode): String {
return when (mode) {
- NoiseControlMode.OFF -> "Off"
- NoiseControlMode.TRANSPARENCY -> "Transparency"
- NoiseControlMode.ADAPTIVE -> "Adaptive"
- NoiseControlMode.NOISE_CANCELLATION -> "Noise Cancel"
+ NoiseControlMode.OFF -> stringResource(R.string.off)
+ NoiseControlMode.TRANSPARENCY -> stringResource(R.string.transparency)
+ NoiseControlMode.ADAPTIVE -> stringResource(R.string.adaptive)
+ NoiseControlMode.NOISE_CANCELLATION -> stringResource(R.string.noise_cancel)
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/NavigationButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/NavigationButton.kt
index c66f2bc6..97cd73e1 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/NavigationButton.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/NavigationButton.kt
@@ -51,6 +51,7 @@ 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.utils.navigateDebounced
@Composable
fun NavigationButton(
@@ -95,7 +96,7 @@ fun NavigationButton(
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
},
onTap = {
- if (onClick != null) onClick() else navController.navigate(to)
+ if (onClick != null) onClick() else navController.navigateDebounced(to)
}
)
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt
index 8566348d..98d723b6 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt
@@ -68,6 +68,7 @@ import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.navigation.NavController
+import me.kavishdevar.librepods.utils.navigateDebounced
import androidx.navigation.compose.rememberNavController
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import com.kyant.backdrop.drawBackdrop
@@ -227,7 +228,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
actionButtons = listOf(
{scaffoldBackdrop ->
StyledIconButton(
- onClick = { navController.navigate("app_settings") },
+ onClick = { navController.navigateDebounced("app_settings") },
icon = "",
darkMode = darkMode,
backdrop = scaffoldBackdrop
@@ -380,7 +381,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
)
Spacer(Modifier.height(32.dp))
StyledButton(
- onClick = { navController.navigate("troubleshooting") },
+ onClick = { navController.navigateDebounced("troubleshooting") },
backdrop = backdrop,
modifier = Modifier
.fillMaxWidth(0.9f)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt
index feac543c..e4e34257 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt
@@ -73,6 +73,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.navigation.NavController
+import me.kavishdevar.librepods.utils.navigateDebounced
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.hazeSource
@@ -655,7 +656,7 @@ fun AppSettingsScreen(navController: NavController) {
onDismissRequest = { showResetDialog.value = false },
title = {
Text(
- "Reset Hook Offset",
+ stringResource(R.string.reset_hook_offset),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
@@ -678,7 +679,7 @@ fun AppSettingsScreen(navController: NavController) {
Toast.LENGTH_LONG
).show()
- navController.navigate("onboarding") {
+ navController.navigateDebounced("onboarding") {
popUpTo("settings") { inclusive = true }
}
} else {
@@ -706,7 +707,7 @@ fun AppSettingsScreen(navController: NavController) {
onClick = { showResetDialog.value = false }
) {
Text(
- "Cancel",
+ stringResource(R.string.cancel),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
@@ -761,10 +762,12 @@ fun AppSettingsScreen(navController: NavController) {
confirmButton = {
val successText = stringResource(R.string.irk_set_success)
val errorText = stringResource(R.string.error_converting_hex)
+ val unknownErrorText = stringResource(R.string.unknown_error)
+ val hexValidationError = stringResource(R.string.must_be_32_hex_chars)
TextButton(
onClick = {
if (!validateHexInput(irkValue.value)) {
- irkError.value = "Must be exactly 32 hex characters"
+ irkError.value = hexValidationError
return@TextButton
}
@@ -781,12 +784,12 @@ fun AppSettingsScreen(navController: NavController) {
Toast.makeText(context, successText, Toast.LENGTH_SHORT).show()
showIrkDialog.value = false
} catch (e: Exception) {
- irkError.value = errorText + " " + (e.message ?: "Unknown error")
+ irkError.value = errorText + " " + (e.message ?: unknownErrorText)
}
}
) {
Text(
- "Save",
+ stringResource(R.string.save),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
@@ -797,7 +800,7 @@ fun AppSettingsScreen(navController: NavController) {
onClick = { showIrkDialog.value = false }
) {
Text(
- "Cancel",
+ stringResource(R.string.cancel),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
@@ -852,10 +855,12 @@ fun AppSettingsScreen(navController: NavController) {
confirmButton = {
val successText = stringResource(R.string.encryption_key_set_success)
val errorText = stringResource(R.string.error_converting_hex)
+ val unknownErrorText = stringResource(R.string.unknown_error)
+ val hexValidationError = stringResource(R.string.must_be_32_hex_chars)
TextButton(
onClick = {
if (!validateHexInput(encKeyValue.value)) {
- encKeyError.value = "Must be exactly 32 hex characters"
+ encKeyError.value = hexValidationError
return@TextButton
}
@@ -872,12 +877,12 @@ fun AppSettingsScreen(navController: NavController) {
Toast.makeText(context, successText, Toast.LENGTH_SHORT).show()
showEncKeyDialog.value = false
} catch (e: Exception) {
- encKeyError.value = errorText + " " + (e.message ?: "Unknown error")
+ encKeyError.value = errorText + " " + (e.message ?: unknownErrorText)
}
}
) {
Text(
- "Save",
+ stringResource(R.string.save),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
@@ -888,7 +893,7 @@ fun AppSettingsScreen(navController: NavController) {
onClick = { showEncKeyDialog.value = false }
) {
Text(
- "Cancel",
+ stringResource(R.string.cancel),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
@@ -957,7 +962,7 @@ fun AppSettingsScreen(navController: NavController) {
}
) {
Text(
- "Save",
+ stringResource(R.string.save),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
@@ -968,7 +973,7 @@ fun AppSettingsScreen(navController: NavController) {
onClick = { showCameraDialog.value = false }
) {
Text(
- "Cancel",
+ stringResource(R.string.cancel),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt
index b956d968..5738f73e 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt
@@ -263,10 +263,10 @@ fun HearingAidScreen(navController: NavController) {
ConfirmationDialog(
showDialog = showDialog,
- title = "Enable Hearing Aid",
- message = "Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode.",
- confirmText = "Enable",
- dismissText = "Cancel",
+ title = stringResource(R.string.enable_hearing_aid),
+ message = stringResource(R.string.enable_hearing_aid_msg),
+ confirmText = stringResource(R.string.enable),
+ dismissText = stringResource(R.string.cancel),
onConfirm = {
showDialog.value = false
val enrolled = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(0) == 0x01.toByte()
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/Onboarding.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/Onboarding.kt
index f735668c..d8c061f1 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/Onboarding.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/Onboarding.kt
@@ -74,6 +74,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.navigation.NavController
+import me.kavishdevar.librepods.utils.navigateDebounced
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
@@ -156,7 +157,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
}
val backdrop = rememberLayerBackdrop()
StyledScaffold(
- title = "Setting Up",
+ title = stringResource(R.string.setting_up),
actionButtons = listOf(
{scaffoldBackdrop ->
StyledIconButton(
@@ -260,7 +261,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
)
} else {
Text(
- "Check Root Access",
+ stringResource(R.string.check_root_access),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
@@ -276,7 +277,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
AnimatedContent(
targetState = if (hasStarted) getStatusTitle(progressState,
- moduleEnabled, bluetoothToggled) else "Setup Required",
+ moduleEnabled, bluetoothToggled) else stringResource(R.string.setup_required),
transitionSpec = { fadeIn() togetherWith fadeOut() }
) { text ->
Text(
@@ -297,7 +298,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
targetState = if (hasStarted)
getStatusDescription(progressState, moduleEnabled, bluetoothToggled)
else
- "AirPods functionality requires one-time setup for hooking into Bluetooth library",
+ stringResource(R.string.setup_required_desc),
transitionSpec = { fadeIn() togetherWith fadeOut() }
) { text ->
Text(
@@ -326,7 +327,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
shape = RoundedCornerShape(8.dp)
) {
Text(
- "Start Setup",
+ stringResource(R.string.start_setup),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
@@ -381,7 +382,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
shape = RoundedCornerShape(8.dp)
) {
Text(
- "I've Enabled/Reactivated the Module",
+ stringResource(R.string.module_enabled_confirm),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
@@ -401,7 +402,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
shape = RoundedCornerShape(8.dp)
) {
Text(
- "I've Toggled Bluetooth",
+ stringResource(R.string.bluetooth_toggled_confirm),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
@@ -412,7 +413,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
} else {
Button(
onClick = {
- navController.navigate("settings") {
+ navController.navigateDebounced("settings") {
popUpTo("onboarding") { inclusive = true }
}
},
@@ -425,7 +426,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
shape = RoundedCornerShape(8.dp)
) {
Text(
- "Continue to Settings",
+ stringResource(R.string.continue_to_settings),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
@@ -475,7 +476,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
shape = RoundedCornerShape(8.dp)
) {
Text(
- "Try Again",
+ stringResource(R.string.try_again),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
@@ -489,10 +490,10 @@ fun Onboarding(navController: NavController, activityContext: Context) {
if (showSkipDialog) {
AlertDialog(
onDismissRequest = { showSkipDialog = false },
- title = { Text("Skip Setup") },
+ title = { Text(stringResource(R.string.skip_setup)) },
text = {
Text(
- "Have you installed the root module that patches the Bluetooth library directly? This option is for users who have manually patched their system instead of using the dynamic hook.",
+ stringResource(R.string.skip_setup_desc),
style = TextStyle(
fontSize = 16.sp,
fontFamily = FontFamily(Font(R.font.sf_pro))
@@ -506,13 +507,13 @@ fun Onboarding(navController: NavController, activityContext: Context) {
showSkipDialog = false
RadareOffsetFinder.clearHookOffsets()
sharedPreferences.edit { putBoolean("skip_setup", true) }
- navController.navigate("settings") {
+ navController.navigateDebounced("settings") {
popUpTo("onboarding") { inclusive = true }
}
}
) {
Text(
- "Yes, Skip Setup",
+ stringResource(R.string.yes_skip_setup),
color = accentColor,
fontWeight = FontWeight.Bold
)
@@ -522,7 +523,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
TextButton(
onClick = { showSkipDialog = false }
) {
- Text("Cancel")
+ Text(stringResource(R.string.cancel))
}
},
containerColor = backgroundColor,
@@ -582,6 +583,7 @@ private fun StatusIcon(
}
}
+@Composable
private fun getStatusTitle(
state: RadareOffsetFinder.ProgressState,
moduleEnabled: Boolean,
@@ -590,24 +592,25 @@ private fun getStatusTitle(
return when (state) {
is RadareOffsetFinder.ProgressState.Success -> {
when {
- !moduleEnabled -> "Enable Xposed Module"
- !bluetoothToggled -> "Toggle Bluetooth"
- else -> "Setup Complete"
+ !moduleEnabled -> stringResource(R.string.enable_xposed_module)
+ !bluetoothToggled -> stringResource(R.string.toggle_bluetooth)
+ else -> stringResource(R.string.setup_complete)
}
}
- is RadareOffsetFinder.ProgressState.Idle -> "Getting Ready"
- is RadareOffsetFinder.ProgressState.CheckingExisting -> "Checking if radare2 already downloaded"
- is RadareOffsetFinder.ProgressState.Downloading -> "Downloading radare2"
- is RadareOffsetFinder.ProgressState.DownloadProgress -> "Downloading radare2"
- is RadareOffsetFinder.ProgressState.Extracting -> "Extracting radare2"
- is RadareOffsetFinder.ProgressState.MakingExecutable -> "Setting executable permissions"
- is RadareOffsetFinder.ProgressState.FindingOffset -> "Finding function offset"
- is RadareOffsetFinder.ProgressState.SavingOffset -> "Saving offset"
- is RadareOffsetFinder.ProgressState.Cleaning -> "Cleaning Up"
- is RadareOffsetFinder.ProgressState.Error -> "Setup Failed"
+ is RadareOffsetFinder.ProgressState.Idle -> stringResource(R.string.getting_ready)
+ is RadareOffsetFinder.ProgressState.CheckingExisting -> stringResource(R.string.checking_radare2)
+ is RadareOffsetFinder.ProgressState.Downloading -> stringResource(R.string.downloading_radare2)
+ is RadareOffsetFinder.ProgressState.DownloadProgress -> stringResource(R.string.downloading_radare2)
+ is RadareOffsetFinder.ProgressState.Extracting -> stringResource(R.string.extracting_radare2)
+ is RadareOffsetFinder.ProgressState.MakingExecutable -> stringResource(R.string.setting_permissions)
+ is RadareOffsetFinder.ProgressState.FindingOffset -> stringResource(R.string.finding_offset)
+ is RadareOffsetFinder.ProgressState.SavingOffset -> stringResource(R.string.saving_offset)
+ is RadareOffsetFinder.ProgressState.Cleaning -> stringResource(R.string.cleaning_up)
+ is RadareOffsetFinder.ProgressState.Error -> stringResource(R.string.setup_failed)
}
}
+@Composable
private fun getStatusDescription(
state: RadareOffsetFinder.ProgressState,
moduleEnabled: Boolean,
@@ -616,20 +619,20 @@ private fun getStatusDescription(
return when (state) {
is RadareOffsetFinder.ProgressState.Success -> {
when {
- !moduleEnabled -> "Please enable the LibrePods Xposed module in your Xposed manager (e.g. LSPosed). If already enabled, disable and re-enable it."
- !bluetoothToggled -> "Please turn off and then turn on Bluetooth to apply the changes."
- else -> "All set! You can now use your AirPods with enhanced functionality."
+ !moduleEnabled -> stringResource(R.string.enable_module_desc)
+ !bluetoothToggled -> stringResource(R.string.toggle_bluetooth_desc)
+ else -> stringResource(R.string.setup_complete_desc)
}
}
- is RadareOffsetFinder.ProgressState.Idle -> "Preparing"
- is RadareOffsetFinder.ProgressState.CheckingExisting -> "Checking if radare2 are already installed"
- is RadareOffsetFinder.ProgressState.Downloading -> "Starting radare2 download"
- is RadareOffsetFinder.ProgressState.DownloadProgress -> "Downloading radare2"
- is RadareOffsetFinder.ProgressState.Extracting -> "Extracting radare2"
- is RadareOffsetFinder.ProgressState.MakingExecutable -> "Setting executable permissions on radare2 binaries"
- is RadareOffsetFinder.ProgressState.FindingOffset -> "Looking for the required Bluetooth function in system libraries"
- is RadareOffsetFinder.ProgressState.SavingOffset -> "Saving the function offset"
- is RadareOffsetFinder.ProgressState.Cleaning -> "Removing temporary extracted files"
+ is RadareOffsetFinder.ProgressState.Idle -> stringResource(R.string.preparing)
+ is RadareOffsetFinder.ProgressState.CheckingExisting -> stringResource(R.string.checking_radare2_desc)
+ is RadareOffsetFinder.ProgressState.Downloading -> stringResource(R.string.downloading_radare2_start)
+ is RadareOffsetFinder.ProgressState.DownloadProgress -> stringResource(R.string.downloading_radare2)
+ is RadareOffsetFinder.ProgressState.Extracting -> stringResource(R.string.extracting_radare2_desc)
+ is RadareOffsetFinder.ProgressState.MakingExecutable -> stringResource(R.string.setting_permissions_desc)
+ is RadareOffsetFinder.ProgressState.FindingOffset -> stringResource(R.string.finding_offset_desc)
+ is RadareOffsetFinder.ProgressState.SavingOffset -> stringResource(R.string.saving_offset_desc)
+ is RadareOffsetFinder.ProgressState.Cleaning -> stringResource(R.string.cleaning_up_desc)
is RadareOffsetFinder.ProgressState.Error -> state.message
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt
index b797ef99..e23366e0 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt
@@ -175,13 +175,13 @@ fun TroubleshootingScreen(navController: NavController) {
outputStream.write(logContent.toByteArray())
}
withContext(Dispatchers.Main) {
- Toast.makeText(context, "Log saved successfully", Toast.LENGTH_SHORT).show()
+ Toast.makeText(context, context.getString(R.string.log_saved), Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
- "Failed to save log: ${e.localizedMessage}",
+ context.getString(R.string.failed_save_log, e.localizedMessage),
Toast.LENGTH_SHORT
).show()
}
@@ -192,11 +192,11 @@ fun TroubleshootingScreen(navController: NavController) {
LaunchedEffect(currentStep) {
instructionText = when (currentStep) {
- 0 -> "First, let's ensure Xposed module is properly configured. Tap the button below to check Xposed scope settings."
- 1 -> "Please put your AirPods in the case and close it, so they disconnect completely."
- 2 -> "Preparing to collect logs... Please wait."
- 3 -> "Now, open the AirPods case and connect your AirPods. Logs are being collected. Connection will be detected automatically, or you can manually stop logging when you're done."
- 4 -> "Log collection complete! You can now save or share the logs."
+ 0 -> context.getString(R.string.troubleshooting_step_0)
+ 1 -> context.getString(R.string.troubleshooting_step_1)
+ 2 -> context.getString(R.string.troubleshooting_step_2)
+ 3 -> context.getString(R.string.troubleshooting_step_3)
+ 4 -> context.getString(R.string.troubleshooting_step_4)
else -> ""
}
}
@@ -288,7 +288,7 @@ fun TroubleshootingScreen(navController: NavController) {
contentColor = MaterialTheme.colorScheme.error
)
) {
- Text("Delete All")
+ Text(stringResource(R.string.delete_all))
}
}
}
@@ -422,7 +422,7 @@ fun TroubleshootingScreen(navController: NavController) {
contentColor = textColor
)
) {
- Text("Open Xposed Settings")
+ Text(stringResource(R.string.open_xposed_settings))
}
}
@@ -479,7 +479,7 @@ fun TroubleshootingScreen(navController: NavController) {
selectedLogFile = it
Toast.makeText(
context,
- "Log saved: ${it.name}",
+ context.getString(R.string.log_saved_name, it.name),
Toast.LENGTH_SHORT
).show()
}
@@ -488,7 +488,7 @@ fun TroubleshootingScreen(navController: NavController) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
- "Error collecting logs: ${e.message}",
+ context.getString(R.string.error_collecting_logs, e.message),
Toast.LENGTH_SHORT
).show()
isCollectingLogs = false
@@ -504,7 +504,7 @@ fun TroubleshootingScreen(navController: NavController) {
contentColor = textColor
)
) {
- Text("Continue")
+ Text(stringResource(R.string.continue_btn))
}
}
@@ -520,7 +520,7 @@ fun TroubleshootingScreen(navController: NavController) {
Spacer(modifier = Modifier.height(8.dp))
Text(
- text = if (currentStep == 2) "Preparing..." else "Collecting logs...",
+ text = if (currentStep == 2) stringResource(R.string.preparing_logs) else stringResource(R.string.collecting_logs),
fontSize = 14.sp,
color = textColor
)
@@ -544,7 +544,7 @@ fun TroubleshootingScreen(navController: NavController) {
isCollectingLogs = false
Toast.makeText(
context,
- "Log collection stopped",
+ context.getString(R.string.log_collection_stopped),
Toast.LENGTH_SHORT
).show()
}
@@ -558,7 +558,7 @@ fun TroubleshootingScreen(navController: NavController) {
modifier = Modifier
.fillMaxWidth()
) {
- Text("Stop Collection")
+ Text(stringResource(R.string.stop_collection))
}
}
}
@@ -606,7 +606,7 @@ fun TroubleshootingScreen(navController: NavController) {
contentDescription = "Share"
)
Spacer(modifier = Modifier.width(8.dp))
- Text("Share")
+ Text(stringResource(R.string.share))
}
Spacer(modifier = Modifier.width(16.dp))
@@ -631,7 +631,7 @@ fun TroubleshootingScreen(navController: NavController) {
contentDescription = "Save"
)
Spacer(modifier = Modifier.width(8.dp))
- Text("Save")
+ Text(stringResource(R.string.save))
}
}
@@ -649,7 +649,7 @@ fun TroubleshootingScreen(navController: NavController) {
contentColor = textColor
)
) {
- Text("Done")
+ Text(stringResource(R.string.done))
}
}
}
@@ -660,9 +660,9 @@ fun TroubleshootingScreen(navController: NavController) {
if (showDeleteDialog && selectedLogFile != null) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
- title = { Text("Delete Log File") },
+ title = { Text(stringResource(R.string.delete_log_file)) },
text = {
- Text("Are you sure you want to delete this log file? This action cannot be undone.")
+ Text(stringResource(R.string.delete_log_confirm))
},
confirmButton = {
TextButton(
@@ -672,14 +672,14 @@ fun TroubleshootingScreen(navController: NavController) {
savedLogs.remove(file)
Toast.makeText(
context,
- "Log file deleted",
+ context.getString(R.string.log_file_deleted),
Toast.LENGTH_SHORT
)
.show()
} else {
Toast.makeText(
context,
- "Failed to delete log file",
+ context.getString(R.string.failed_delete_log),
Toast.LENGTH_SHORT
).show()
}
@@ -687,12 +687,12 @@ fun TroubleshootingScreen(navController: NavController) {
showDeleteDialog = false
}
) {
- Text("Delete", color = MaterialTheme.colorScheme.error)
+ Text(stringResource(R.string.delete), color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = false }) {
- Text("Cancel")
+ Text(stringResource(R.string.cancel))
}
}
)
@@ -701,9 +701,9 @@ fun TroubleshootingScreen(navController: NavController) {
if (showDeleteAllDialog) {
AlertDialog(
onDismissRequest = { showDeleteAllDialog = false },
- title = { Text("Delete All Logs") },
+ title = { Text(stringResource(R.string.delete_all_logs)) },
text = {
- Text("Are you sure you want to delete all log files? This action cannot be undone and will remove ${savedLogs.size} log files.")
+ Text(stringResource(R.string.delete_all_logs_confirm, savedLogs.size))
},
confirmButton = {
TextButton(
@@ -720,13 +720,13 @@ fun TroubleshootingScreen(navController: NavController) {
savedLogs.clear()
Toast.makeText(
context,
- "Deleted $deletedCount log files",
+ context.getString(R.string.deleted_logs_count, deletedCount),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
- "Failed to delete log files",
+ context.getString(R.string.failed_delete_logs),
Toast.LENGTH_SHORT
).show()
}
@@ -735,12 +735,12 @@ fun TroubleshootingScreen(navController: NavController) {
showDeleteAllDialog = false
}
) {
- Text("Delete All", color = MaterialTheme.colorScheme.error)
+ Text(stringResource(R.string.delete_all), color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
TextButton(onClick = { showDeleteAllDialog = false }) {
- Text("Cancel")
+ Text(stringResource(R.string.cancel))
}
}
)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt
index fb849982..2f728a7f 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt
@@ -214,7 +214,7 @@ class AirPodsQSService : TileService() {
} else {
tile.state = Tile.STATE_UNAVAILABLE
tile.label = "AirPods"
- tile.subtitle = "Disconnected"
+ tile.subtitle = getString(R.string.disconnected)
tile.icon = Icon.createWithResource(this, R.drawable.airpods)
}
@@ -251,10 +251,10 @@ class AirPodsQSService : TileService() {
private fun getModeLabel(mode: Int): String {
return when (mode) {
- NoiseControlMode.OFF.ordinal + 1 -> "Off"
- NoiseControlMode.TRANSPARENCY.ordinal + 1 -> "Transparency"
- NoiseControlMode.ADAPTIVE.ordinal + 1 -> "Adaptive"
- NoiseControlMode.NOISE_CANCELLATION.ordinal + 1 -> "Noise Cancellation"
+ NoiseControlMode.OFF.ordinal + 1 -> getString(R.string.off)
+ NoiseControlMode.TRANSPARENCY.ordinal + 1 -> getString(R.string.transparency)
+ NoiseControlMode.ADAPTIVE.ordinal + 1 -> getString(R.string.adaptive)
+ NoiseControlMode.NOISE_CANCELLATION.ordinal + 1 -> getString(R.string.noise_cancellation)
else -> "Unknown"
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt
index d890e88f..61769281 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt
@@ -1498,7 +1498,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
"AirPods Socket Connection Issues",
NotificationManager.IMPORTANCE_HIGH
).apply {
- description = "Notifications about problems connecting to AirPods protocol"
+ description = getString(R.string.notification_channel_issues)
enableLights(true)
lightColor = Color.RED
enableVibration(true)
@@ -1522,8 +1522,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val notification = NotificationCompat.Builder(this, "background_service_status")
.setSmallIcon(R.drawable.airpods)
- .setContentTitle("Background Service Running")
- .setContentText("Useless notification, disable it by clicking on it.")
+ .setContentTitle(getString(R.string.background_service_running))
+ .setContentText(getString(R.string.useless_notification))
.setContentIntent(pendingIntentNotifDisable)
.setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW)
@@ -1551,10 +1551,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val notification = NotificationCompat.Builder(this, "socket_connection_failure")
.setSmallIcon(R.drawable.airpods)
- .setContentTitle("AirPods Connection Issue")
- .setContentText("Unable to connect to AirPods over L2CAP")
+ .setContentTitle(getString(R.string.airpods_connection_issue))
+ .setContentText(getString(R.string.l2cap_connection_failed))
.setStyle(NotificationCompat.BigTextStyle()
- .bigText("Your AirPods are connected via Bluetooth, but LibrePods couldn't connect to AirPods using L2CAP. " +
+ .bigText(getString(R.string.l2cap_connection_failed_detail) + " " +
"Error: $errorMessage"))
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_ERROR)
@@ -1857,8 +1857,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} else if (!connected) {
updatedNotification = NotificationCompat.Builder(this, "background_service_status")
.setSmallIcon(R.drawable.airpods)
- .setContentTitle("AirPods not connected")
- .setContentText("Tap to open app")
+ .setContentTitle(getString(R.string.airpods_not_connected))
+ .setContentText(getString(R.string.tap_to_open_app))
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/NavigationExtensions.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/NavigationExtensions.kt
new file mode 100644
index 00000000..bd02ff40
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/NavigationExtensions.kt
@@ -0,0 +1,76 @@
+/*
+ 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 General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ any later version.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+package me.kavishdevar.librepods.utils
+
+import androidx.navigation.NavController
+import androidx.navigation.NavOptionsBuilder
+
+/**
+ * Navigation debounce manager to prevent rapid repeated navigation.
+ */
+private object NavigationDebounce {
+ private var lastNavigationTime: Long = 0
+ private var lastRoute: String? = null
+ private const val DEBOUNCE_INTERVAL_MS = 300L
+
+ fun shouldNavigate(route: String): Boolean {
+ val currentTime = System.currentTimeMillis()
+ if (route == lastRoute && currentTime - lastNavigationTime < DEBOUNCE_INTERVAL_MS) {
+ return false
+ }
+ lastNavigationTime = currentTime
+ lastRoute = route
+ return true
+ }
+}
+
+/**
+ * Navigate with debounce protection.
+ * Rapid clicks to the same route within 300ms will be ignored.
+ */
+fun NavController.navigateDebounced(route: String): Boolean {
+ if (!NavigationDebounce.shouldNavigate(route)) {
+ return false
+ }
+ navigate(route)
+ return true
+}
+
+/**
+ * Navigate with debounce protection and custom options.
+ */
+fun NavController.navigateDebounced(route: String, builder: NavOptionsBuilder.() -> Unit): Boolean {
+ if (!NavigationDebounce.shouldNavigate(route)) {
+ return false
+ }
+ navigate(route, builder)
+ return true
+}
+
+/**
+ * Safely pop the back stack with boundary check.
+ * Returns false if there's no page to go back to.
+ */
+fun NavController.popBackStackSafely(): Boolean {
+ // Check if there's a previous destination to go back to
+ if (previousBackStackEntry != null) {
+ return popBackStack()
+ }
+ return false
+}
diff --git a/android/app/src/main/res/values-zh-rCN/strings.xml b/android/app/src/main/res/values-zh-rCN/strings.xml
index 3178aba7..71eeaaaa 100644
--- a/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -215,4 +215,106 @@
允许外部声音进入
动态调整外部噪音
阻隔外部声音
+ 需要权限
+ 蓝牙权限
+ 与 AirPods 通信所必需
+ 通知权限
+ 用于显示电池状态
+ 电话权限
+ 用于通过头部手势接听电话
+ 显示在其他应用上层
+ 用于 AirPods 连接时的弹出动画
+ 请求常规权限
+ 悬浮窗权限已授予
+ 授予悬浮窗权限
+ 不使用悬浮窗继续
+
+ 正在设置
+ 检查 Root 权限
+ 需要设置
+ AirPods 功能需要一次性设置以挂钩蓝牙库
+ 开始设置
+ 我已启用/重新激活模块
+ 我已开关蓝牙
+ 继续进入设置
+ 重试
+ 跳过设置
+ 你是否已安装了直接修补蓝牙库的 Root 模块?此选项适用于手动修补系统而非使用动态挂钩的用户。
+ 是的,跳过设置
+ 取消
+
+ 启用 Xposed 模块
+ 开关蓝牙
+ 设置完成
+ 正在准备
+ 检查 radare2 是否已下载
+ 正在下载 radare2
+ 正在解压 radare2
+ 正在设置执行权限
+ 正在查找函数偏移
+ 正在保存偏移
+ 正在清理
+ 设置失败
+
+ 请在你的 Xposed 管理器(如 LSPosed)中启用 LibrePods Xposed 模块。如果已启用,请禁用后重新启用。
+ 请关闭然后打开蓝牙以应用更改。
+ 设置完成!你现在可以使用增强功能的 AirPods 了。
+ 正在准备
+ 检查 radare2 是否已安装
+ 正在开始下载 radare2
+ 正在解压 radare2
+ 正在为 radare2 二进制文件设置执行权限
+ 正在系统库中查找所需的蓝牙函数
+ 正在保存函数偏移
+ 正在删除临时解压文件
+
+ 全部删除
+ 打开 Xposed 设置
+ 继续
+ 正在准备...
+ 正在收集日志...
+ 停止收集
+ 分享
+ 保存
+ 完成
+ 删除
+ 删除日志文件
+ 确定要删除此日志文件吗?此操作无法撤销。
+ 删除所有日志
+ 确定要删除所有日志文件吗?此操作无法撤销,将删除 %d 个日志文件。
+ 日志保存成功
+ 日志已保存:%s
+ 日志收集已停止
+ 日志文件已删除
+ 删除日志文件失败
+ 已删除 %d 个日志文件
+ 删除日志文件失败
+ 收集日志出错:%s
+ 分享日志文件
+ 日志总数:%d
+
+ 启用助听
+ 启用助听将禁用耳机调节和自定义通透模式。
+ 启用
+
+ 加载中...
+ 降噪
+
+ 后台服务运行中
+ 无用的通知,点击以禁用。
+ AirPods 连接问题
+ 无法通过 L2CAP 连接 AirPods
+ 你的 AirPods 已通过蓝牙连接,但 LibrePods 无法通过 L2CAP 连接。这通常发生在另一台设备已打开 L2CAP 连接时。请尝试先断开 AirPods 与其他设备的连接。
+ 点击打开应用
+ 关于 AirPods 协议连接问题的通知
+
+ 未连接
+
+ 首先,让我们确保 Xposed 模块已正确配置。点击下方按钮检查 Xposed 作用域设置。
+ 请将 AirPods 放入充电盒并关闭盒盖,使其完全断开连接。
+ 正在准备收集日志...请稍候。
+ 现在,打开 AirPods 充电盒并连接。正在收集日志。连接将自动检测,或者你可以在完成后手动停止日志记录。
+ 日志收集完成!你现在可以保存或分享日志。
+ 保存日志失败:%s
+ 未知错误
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 74ae083a..063c0e44 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -214,4 +214,106 @@
Lets in external sounds
Dynamically adjust external noise
Blocks out external sounds
+ Permission Required
+ Bluetooth Permissions
+ Required to communicate with your AirPods
+ Notification Permission
+ To show battery status
+ Phone Permissions
+ For answering calls with Head Gestures
+ Display Over Other Apps
+ For popup animations when AirPods connect
+ Ask for regular permissions
+ Overlay Permission Granted
+ Grant Overlay Permission
+ Continue without overlay
+
+ Setting Up
+ Check Root Access
+ Setup Required
+ AirPods functionality requires one-time setup for hooking into Bluetooth library
+ Start Setup
+ I\'ve Enabled/Reactivated the Module
+ I\'ve Toggled Bluetooth
+ Continue to Settings
+ Try Again
+ Skip Setup
+ Have you installed the root module that patches the Bluetooth library directly? This option is for users who have manually patched their system instead of using the dynamic hook.
+ Yes, Skip Setup
+ Cancel
+
+ Enable Xposed Module
+ Toggle Bluetooth
+ Setup Complete
+ Getting Ready
+ Checking if radare2 already downloaded
+ Downloading radare2
+ Extracting radare2
+ Setting executable permissions
+ Finding function offset
+ Saving offset
+ Cleaning Up
+ Setup Failed
+
+ Please enable the LibrePods Xposed module in your Xposed manager (e.g. LSPosed). If already enabled, disable and re-enable it.
+ Please turn off and then turn on Bluetooth to apply the changes.
+ All set! You can now use your AirPods with enhanced functionality.
+ Preparing
+ Checking if radare2 are already installed
+ Starting radare2 download
+ Extracting radare2
+ Setting executable permissions on radare2 binaries
+ Looking for the required Bluetooth function in system libraries
+ Saving the function offset
+ Removing temporary extracted files
+
+ Delete All
+ Open Xposed Settings
+ Continue
+ Preparing...
+ Collecting logs...
+ Stop Collection
+ Share
+ Save
+ Done
+ Delete
+ Delete Log File
+ Are you sure you want to delete this log file? This action cannot be undone.
+ Delete All Logs
+ Are you sure you want to delete all log files? This action cannot be undone and will remove %d log files.
+ Log saved successfully
+ Log saved: %s
+ Log collection stopped
+ Log file deleted
+ Failed to delete log file
+ Deleted %d log files
+ Failed to delete log files
+ Error collecting logs: %s
+ Share log file
+ Total Logs: %d
+
+ Enable Hearing Aid
+ Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode.
+ Enable
+
+ Loading...
+ Noise Cancel
+
+ Background Service Running
+ Useless notification, disable it by clicking on it.
+ AirPods Connection Issue
+ Unable to connect to AirPods over L2CAP
+ Your AirPods are connected via Bluetooth, but LibrePods couldn\'t connect to AirPods using L2CAP. This usually happens when another device already has an L2CAP connection open. Try disconnecting the AirPods from other devices first.
+ Tap to open app
+ Notifications about problems connecting to AirPods protocol
+
+ Disconnected
+
+ First, let\'s ensure Xposed module is properly configured. Tap the button below to check Xposed scope settings.
+ Please put your AirPods in the case and close it, so they disconnect completely.
+ Preparing to collect logs... Please wait.
+ Now, open the AirPods case and connect your AirPods. Logs are being collected. Connection will be detected automatically, or you can manually stop logging when you\'re done.
+ Log collection complete! You can now save or share the logs.
+ Failed to save log: %s
+ Unknown error