diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0474dfd88..e5f39c1c2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -116,7 +116,7 @@ android:name=".services.AirPodsQSService" android:exported="true" android:icon="@drawable/airpods" - android:label="ANC Mode" + android:label="@string/anc_mode" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> 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 2aa9158c3..33efa074c 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt @@ -20,6 +20,7 @@ package me.kavishdevar.librepods +import androidx.compose.ui.res.stringResource import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.ComponentName @@ -428,7 +429,7 @@ fun NewControlCenterDialogContent( ) { Icon( painter = painterResource(id = R.drawable.airpods), - contentDescription = "Device Icon", + contentDescription = stringResource(R.string.device_icon), tint = textColor.copy(alpha = 0.8f), modifier = Modifier.size(48.dp) ) @@ -588,7 +589,7 @@ fun NewControlCenterDialogContent( ) { Icon( painter = painterResource(id = R.drawable.airpods), - contentDescription = "Conversational Awareness", + contentDescription = stringResource(R.string.conversational_awareness), tint = Color.White, modifier = Modifier.size(32.dp) ) @@ -597,7 +598,7 @@ fun NewControlCenterDialogContent( Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Conversational\nAwareness", + text = stringResource(R.string.conversational_awareness), 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_cancellation) } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProvider.kt b/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProvider.kt index 52a670512..1e35458fa 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProvider.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProvider.kt @@ -21,6 +21,10 @@ package me.kavishdevar.librepods.billing import android.app.Activity import kotlinx.coroutines.flow.StateFlow +/** Internal price states; UI resolves them in the current locale. */ +const val PRICE_LOADING = "__price_loading__" +const val PRICE_UNAVAILABLE = "__price_unavailable__" + interface BillingProvider { val isPremium: StateFlow val price: StateFlow diff --git a/android/app/src/main/java/me/kavishdevar/librepods/billing/PlayBillingProvider.kt b/android/app/src/main/java/me/kavishdevar/librepods/billing/PlayBillingProvider.kt index 02f17b95e..30286f571 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/billing/PlayBillingProvider.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/billing/PlayBillingProvider.kt @@ -55,7 +55,7 @@ class PlayBillingProvider( private val _isPremium = MutableStateFlow(false) override val isPremium: StateFlow = _isPremium - private val _price = MutableStateFlow("unknown") + private val _price = MutableStateFlow(PRICE_LOADING) override val price: StateFlow = _price @@ -81,6 +81,7 @@ class PlayBillingProvider( queryExistingPurchases() } } else { + _price.value = PRICE_UNAVAILABLE Log.w(TAG, "Billing setup failed: ${result.debugMessage}") } } @@ -112,8 +113,11 @@ class PlayBillingProvider( if (priceString != null) { _price.value = priceString + } else { + _price.value = PRICE_UNAVAILABLE } } else { + _price.value = PRICE_UNAVAILABLE Log.w(TAG, "queryProductDetails failed: ${result.billingResult.debugMessage}") } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt index ca0d1f4c8..959c19ac2 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt @@ -18,6 +18,7 @@ package me.kavishdevar.librepods.presentation.components +import androidx.compose.ui.res.stringResource import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.spring @@ -231,12 +232,13 @@ 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 Cancellation" + 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_cancellation) } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt index d0df910e8..b27223f80 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt @@ -32,13 +32,13 @@ fun PressAndHoldSettings( ) { val leftActionText = when (leftAction) { StemAction.CYCLE_NOISE_CONTROL_MODES -> stringResource(R.string.noise_control) - StemAction.DIGITAL_ASSISTANT -> "Digital Assistant" + StemAction.DIGITAL_ASSISTANT -> stringResource(R.string.digital_assistant) else -> "INVALID!!" } val rightActionText = when (rightAction) { StemAction.CYCLE_NOISE_CONTROL_MODES -> stringResource(R.string.noise_control) - StemAction.DIGITAL_ASSISTANT -> "Digital Assistant" + StemAction.DIGITAL_ASSISTANT -> stringResource(R.string.digital_assistant) else -> "INVALID!!" } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/navigation/NavigationRoot.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/navigation/NavigationRoot.kt index 2bca355a1..e0935baa3 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/navigation/NavigationRoot.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/navigation/NavigationRoot.kt @@ -89,7 +89,7 @@ fun NavigationRoot( ) { Icon( imageVector = Icons.Outlined.Settings, - contentDescription = "settings", + contentDescription = stringResource(R.string.settings), modifier = Modifier.size(IconButtonDefaults.mediumIconSize) ) } @@ -115,7 +115,7 @@ fun NavigationRoot( ) { Icon( imageVector = if (state.headTrackingActive) MaterialIcons.pause else Icons.Default.PlayArrow, - contentDescription = "Play/Pause", + contentDescription = stringResource(R.string.play_pause), modifier = Modifier.size(IconButtonDefaults.mediumIconSize) ) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt index b607817c3..2f8a53a6c 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt @@ -281,10 +281,10 @@ fun AirPodsSettingsScreen( val emailIntent = Intent(Intent.ACTION_SENDTO).apply { data = "mailto:".toUri() putExtra(Intent.EXTRA_EMAIL, arrayOf("billing@kavish.xyz")) - putExtra(Intent.EXTRA_SUBJECT, "LibrePods Play billing error") + putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.billing_error_email_subject)) putExtra( Intent.EXTRA_TEXT, - "Please enter your GitHub username to restore your premium access:\n\nGitHub username: " + context.getString(R.string.billing_error_email_body) ) } context.startActivity(emailIntent) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt index 06436561d..bfb647c3c 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt @@ -160,10 +160,10 @@ fun AppSettingsScreen( val emailIntent = Intent(Intent.ACTION_SENDTO).apply { data = "mailto:".toUri() putExtra(Intent.EXTRA_EMAIL, arrayOf("billing@kavish.xyz")) - putExtra(Intent.EXTRA_SUBJECT, "LibrePods Play billing error") + putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.billing_error_email_subject)) putExtra( Intent.EXTRA_TEXT, - "Please enter your GitHub username to restore your premium access:\n\nGitHub username: " + context.getString(R.string.billing_error_email_body) ) } context.startActivity(emailIntent) @@ -570,13 +570,13 @@ fun AppSettingsScreen( Intent.EXTRA_TEXT, "${descriptionState.text}" + "\n\n----------" + - "\nPhone details:" + + "\n" + context.getString(R.string.contact_phone_details) + "\nMANUFACTURER: ${Build.MANUFACTURER}" + "\nMODEL: ${Build.MODEL} (${Build.PRODUCT})" + "\nDISPLAY_VERSION: ${Build.DISPLAY}" + "\nID: ${Build.ID} (SDK ${Build.VERSION.SDK_INT_FULL})" + "\nXposed enabled/active: ${XposedState.isAvailable}/${XposedState.bluetoothScopeEnabled}" + - "\n\nApp details:" + + "\n\n" + context.getString(R.string.contact_app_details) + "\nVERSION: ${BuildConfig.VERSION_NAME}" + "\nVERSION_CODE: ${BuildConfig.VERSION_CODE}" + "\nFLAVOR: ${BuildConfig.FLAVOR}" + diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/EqualizerScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/EqualizerScreen.kt index 408e6f575..c08543b35 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/EqualizerScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/EqualizerScreen.kt @@ -541,7 +541,7 @@ fun EqualizerCard( modifier = Modifier.weight(1f) ) { Text( - text = "Low".uppercase(), + text = stringResource(R.string.equalizer_low).uppercase(), style = TextStyle( fontSize = 14.sp, fontFamily = FontFamily(Font(R.font.sf_pro)), @@ -558,7 +558,7 @@ fun EqualizerCard( modifier = Modifier.weight(1f) ) { Text( - text = "Mid".uppercase(), + text = stringResource(R.string.equalizer_mid).uppercase(), style = TextStyle( fontSize = 14.sp, fontFamily = FontFamily(Font(R.font.sf_pro)), @@ -575,7 +575,7 @@ fun EqualizerCard( modifier = Modifier.weight(1f) ) { Text( - text = "High".uppercase(), + text = stringResource(R.string.equalizer_high).uppercase(), style = TextStyle( fontSize = 14.sp, fontFamily = FontFamily(Font(R.font.sf_pro)), diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt index 79a14af1f..e9e8afb40 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt @@ -159,7 +159,7 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel, navigateToPurchase: () -> Un } StyledToggle( - label = "Head Gestures", + label = stringResource(R.string.head_gestures), checked = state.headGesturesEnabled, onCheckedChange = { viewModel.setHeadGesturesEnabled(it) }, enabled = state.isPremium || state.headGesturesEnabled, @@ -171,7 +171,7 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel, navigateToPurchase: () -> Un Spacer(modifier = Modifier.height(16.dp)) Text( - "Velocity", + stringResource(R.string.velocity), style = TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Bold, @@ -195,12 +195,14 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel, navigateToPurchase: () -> Un } } val gestureTextValue = stringResource(R.string.shake_your_head_or_nod) + val yesGestureDetected = stringResource(R.string.yes_gesture_detected) + val noGestureDetected = stringResource(R.string.no_gesture_detected) StyledButton( onClick = { gestureText = gestureTextValue coroutineScope.launch { val accepted = ServiceManager.getService()?.testHeadGestures() ?: false - gestureText = if (accepted) "\"Yes\" gesture detected." else "\"No\" gesture detected." + gestureText = if (accepted) yesGestureDetected else noGestureDetected } }, backdrop = backdrop, @@ -210,7 +212,7 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel, navigateToPurchase: () -> Un maxScale = 0.05f ) { Text( - "Test Head Gestures", + stringResource(R.string.test_head_gestures), style = TextStyle( fontSize = 16.sp, fontWeight = FontWeight.Medium, @@ -277,6 +279,8 @@ private fun Plot() { val maxPoints = 100 val points = remember { mutableStateListOf>() } val darkTheme = isSystemInDarkTheme() + val horizontalLabel = stringResource(R.string.horizontal) + val verticalLabel = stringResource(R.string.vertical) var maxAbs by remember { mutableFloatStateOf(1000f) } @@ -387,7 +391,7 @@ private fun Plot() { textSize = 12.sp.toPx() textAlign = Paint.Align.LEFT } - drawText("Horizontal", width - 140.dp.toPx(), textOffsetY, paint) + drawText(horizontalLabel, width - 140.dp.toPx(), textOffsetY, paint) } drawCircle(verticalColor, 5.dp.toPx(), Offset(width - 70.dp.toPx(), legendY)) @@ -397,7 +401,7 @@ private fun Plot() { textSize = 12.sp.toPx() textAlign = Paint.Align.LEFT } - drawText("Vertical", width - 60.dp.toPx(), textOffsetY, paint) + drawText(verticalLabel, width - 60.dp.toPx(), textOffsetY, paint) } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidScreen.kt index 4a040a161..2fade912f 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidScreen.kt @@ -202,10 +202,10 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, onNavigateHearingAidAdjustment 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_confirmation), + confirmText = stringResource(R.string.enable), + dismissText = stringResource(R.string.cancel), onConfirm = { showDialog.value = false val enrolled = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(0) == 0x01.toByte() diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt index 247eacb5f..df35be8d0 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt @@ -157,7 +157,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navigateToPurchase: () leadingContent = { Icon( painter = painterResource(R.drawable.noise_cancellation), - contentDescription = "Icon", + contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier .height(42.dp) @@ -178,7 +178,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navigateToPurchase: () leadingContent = { Icon( painter = painterResource(R.drawable.transparency), - contentDescription = "Icon", + contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier .height(42.dp) @@ -198,7 +198,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navigateToPurchase: () leadingContent = { Icon( painter = painterResource(R.drawable.adaptive), - contentDescription = "Icon", + contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier .height(42.dp) @@ -218,7 +218,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navigateToPurchase: () leadingContent = { Icon( painter = painterResource(R.drawable.noise_cancellation), - contentDescription = "Icon", + contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier .height(42.dp) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt index c5d1642c3..425ac3364 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt @@ -45,6 +45,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop import me.kavishdevar.librepods.R +import me.kavishdevar.librepods.billing.PRICE_LOADING +import me.kavishdevar.librepods.billing.PRICE_UNAVAILABLE import me.kavishdevar.librepods.presentation.components.ListItemOrientation import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle import me.kavishdevar.librepods.presentation.components.StyledButton @@ -188,7 +190,11 @@ fun PurchaseScreen( materialButtonStyle = MaterialButtonStyle.Filled ) { Text( - stringResource(R.string.buy_price, state.price), + when (state.price) { + PRICE_LOADING -> stringResource(R.string.price_loading) + PRICE_UNAVAILABLE -> stringResource(R.string.price_unavailable) + else -> stringResource(R.string.buy_price, state.price) + }, style = MaterialTheme.typography.bodyMediumEmphasized, color = MaterialTheme.colorScheme.onPrimary ) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TroubleshootingScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TroubleshootingScreen.kt index 38d299e36..a8ff41ef9 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TroubleshootingScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TroubleshootingScreen.kt @@ -98,6 +98,7 @@ import kotlinx.coroutines.withContext import me.kavishdevar.librepods.R import me.kavishdevar.librepods.utils.LogCollector import java.io.File +import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -176,13 +177,13 @@ fun TroubleshootingScreen() { 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_successfully), Toast.LENGTH_SHORT).show() } } catch (e: Exception) { withContext(Dispatchers.Main) { Toast.makeText( context, - "Failed to save log: ${e.localizedMessage}", + context.getString(R.string.log_save_failed, e.localizedMessage), Toast.LENGTH_SHORT ).show() } @@ -193,11 +194,11 @@ fun TroubleshootingScreen() { 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_xposed) + 1 -> context.getString(R.string.troubleshooting_step_disconnect) + 2 -> context.getString(R.string.troubleshooting_step_prepare) + 3 -> context.getString(R.string.troubleshooting_step_connect) + 4 -> context.getString(R.string.troubleshooting_step_complete) else -> "" } } @@ -276,7 +277,7 @@ fun TroubleshootingScreen() { verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Total Logs: ${savedLogs.size}", + text = context.resources.getQuantityString(R.plurals.total_logs, savedLogs.size, savedLogs.size), fontSize = 16.sp, fontWeight = FontWeight.Medium, color = textColor @@ -289,7 +290,7 @@ fun TroubleshootingScreen() { contentColor = MaterialTheme.colorScheme.error ) ) { - Text("Delete All") + Text(stringResource(R.string.delete_all)) } } } @@ -314,7 +315,7 @@ fun TroubleshootingScreen() { ) Text( - text = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.US) + text = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) .format(Date(logFile.lastModified())), fontSize = 14.sp, color = textColor.copy(alpha = 0.6f) @@ -329,7 +330,7 @@ fun TroubleshootingScreen() { ) { Icon( Icons.Default.Delete, - contentDescription = "Delete", + contentDescription = stringResource(R.string.delete), tint = MaterialTheme.colorScheme.error ) } @@ -423,7 +424,7 @@ fun TroubleshootingScreen() { contentColor = textColor ) ) { - Text("Open Xposed Settings") + Text(stringResource(R.string.open_xposed_settings)) } } @@ -480,7 +481,7 @@ fun TroubleshootingScreen() { selectedLogFile = it Toast.makeText( context, - "Log saved: ${it.name}", + context.getString(R.string.log_saved, it.name), Toast.LENGTH_SHORT ).show() } @@ -489,7 +490,7 @@ fun TroubleshootingScreen() { withContext(Dispatchers.Main) { Toast.makeText( context, - "Error collecting logs: ${e.message}", + context.getString(R.string.log_collection_failed, e.message), Toast.LENGTH_SHORT ).show() isCollectingLogs = false @@ -505,7 +506,7 @@ fun TroubleshootingScreen() { contentColor = textColor ) ) { - Text("Continue") + Text(stringResource(R.string.continue_action)) } } @@ -521,7 +522,7 @@ fun TroubleshootingScreen() { Spacer(modifier = Modifier.height(8.dp)) Text( - text = if (currentStep == 2) "Preparing..." else "Collecting logs...", + text = if (currentStep == 2) stringResource(R.string.preparing) else stringResource(R.string.collecting_logs), fontSize = 14.sp, color = textColor ) @@ -545,7 +546,7 @@ fun TroubleshootingScreen() { isCollectingLogs = false Toast.makeText( context, - "Log collection stopped", + context.getString(R.string.log_collection_stopped), Toast.LENGTH_SHORT ).show() } @@ -559,7 +560,7 @@ fun TroubleshootingScreen() { modifier = Modifier .fillMaxWidth() ) { - Text("Stop Collection") + Text(stringResource(R.string.stop_collection)) } } } @@ -590,7 +591,7 @@ fun TroubleshootingScreen() { context.startActivity( Intent.createChooser( shareIntent, - "Share log file" + context.getString(R.string.share_log_file) ) ) } @@ -604,10 +605,10 @@ fun TroubleshootingScreen() { ) { Icon( imageVector = Icons.Default.Share, - contentDescription = "Share" + contentDescription = stringResource(R.string.share) ) Spacer(modifier = Modifier.width(8.dp)) - Text("Share") + Text(stringResource(R.string.share)) } Spacer(modifier = Modifier.width(16.dp)) @@ -629,10 +630,10 @@ fun TroubleshootingScreen() { ) { Icon( painter = painterResource(id = R.drawable.ic_save), - contentDescription = "Save" + contentDescription = stringResource(R.string.save) ) Spacer(modifier = Modifier.width(8.dp)) - Text("Save") + Text(stringResource(R.string.save)) } } @@ -650,7 +651,7 @@ fun TroubleshootingScreen() { contentColor = textColor ) ) { - Text("Done") + Text(stringResource(R.string.done)) } } } @@ -661,9 +662,9 @@ fun TroubleshootingScreen() { 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_file_confirmation)) }, confirmButton = { TextButton( @@ -673,14 +674,14 @@ fun TroubleshootingScreen() { 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.delete_log_file_failed), Toast.LENGTH_SHORT ).show() } @@ -688,12 +689,12 @@ fun TroubleshootingScreen() { 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)) } } ) @@ -702,9 +703,9 @@ fun TroubleshootingScreen() { 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(context.resources.getQuantityString(R.plurals.delete_all_logs_confirmation, savedLogs.size, savedLogs.size)) }, confirmButton = { TextButton( @@ -721,13 +722,13 @@ fun TroubleshootingScreen() { savedLogs.clear() Toast.makeText( context, - "Deleted $deletedCount log files", + context.resources.getQuantityString(R.plurals.deleted_log_files, deletedCount, deletedCount), Toast.LENGTH_SHORT ).show() } else { Toast.makeText( context, - "Failed to delete log files", + context.getString(R.string.delete_log_files_failed), Toast.LENGTH_SHORT ).show() } @@ -736,12 +737,12 @@ fun TroubleshootingScreen() { 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)) } } ) @@ -766,7 +767,7 @@ fun TroubleshootingScreen() { logContent = try { selectedLogFile?.readText() ?: "" } catch (e: Exception) { - "Error loading log content: ${e.message}" + context.getString(R.string.log_content_load_failed, e.message) } isLoadingLogContent = false logContentLoaded = true @@ -786,7 +787,7 @@ fun TroubleshootingScreen() { .padding(bottom = 12.dp), ) { Text( - text = selectedLogFile?.name ?: "Log Content", + text = selectedLogFile?.name ?: stringResource(R.string.log_content), style = TextStyle( fontWeight = FontWeight.Bold, fontSize = 20.sp, @@ -795,7 +796,7 @@ fun TroubleshootingScreen() { color = textColor ) Text( - text = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.US) + text = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) .format(Date(selectedLogFile?.lastModified() ?: 0)), fontSize = 14.sp, color = textColor.copy(alpha = 0.7f), @@ -866,7 +867,7 @@ fun TroubleshootingScreen() { context.startActivity( Intent.createChooser( shareIntent, - "Share log file" + context.getString(R.string.share_log_file) ) ) } @@ -880,10 +881,10 @@ fun TroubleshootingScreen() { ) { Icon( imageVector = Icons.Default.Share, - contentDescription = "Share" + contentDescription = stringResource(R.string.share) ) Spacer(modifier = Modifier.width(8.dp)) - Text("Share") + Text(stringResource(R.string.share)) } Button( @@ -901,10 +902,10 @@ fun TroubleshootingScreen() { ) { Icon( painter = painterResource(id = R.drawable.ic_save), - contentDescription = "Save" + contentDescription = stringResource(R.string.save) ) Spacer(modifier = Modifier.width(8.dp)) - Text("Save") + Text(stringResource(R.string.save)) } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/OnboardingScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/OnboardingScreen.kt index ae7149dc5..3d6cf9ba4 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/OnboardingScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/OnboardingScreen.kt @@ -136,7 +136,7 @@ fun OnboardingScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "Welcome to", + text = stringResource(R.string.welcome_to), style = MaterialTheme.typography.displayLarge, color = MaterialTheme.colorScheme.primary, textAlign = TextAlign.Center, @@ -172,7 +172,7 @@ fun OnboardingScreen( ) { Icon( Icons.AutoMirrored.Default.ArrowForward, - contentDescription = "forward", + contentDescription = stringResource(R.string.navigate_forward), modifier = Modifier.size(IconButtonDefaults.largeIconSize), ) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/PermissionsPage.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/PermissionsPage.kt index 8f428d0ba..4571a29a4 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/PermissionsPage.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/PermissionsPage.kt @@ -1,5 +1,7 @@ package me.kavishdevar.librepods.presentation.screens.onboarding +import me.kavishdevar.librepods.R +import androidx.compose.ui.res.stringResource import android.content.Intent import android.provider.Settings import androidx.compose.animation.animateColorAsState @@ -136,21 +138,21 @@ fun PermissionsPage( .verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - StyledList(title = "Required Permissions") { + StyledList(title = stringResource(R.string.required_permissions)) { val animatedBluetoothIconColor by animateColorAsState(if (bluetoothPermissionsState.allPermissionsGranted) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface) val animatedBluetoothContainerColor by animateColorAsState( if (bluetoothPermissionsState.allPermissionsGranted) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest ) StyledListItem( - name = "Bluetooth", + name = stringResource(R.string.bluetooth), onClick = if (!bluetoothPermissionsState.allPermissionsGranted) { { grantingAll = false bluetoothPermissionsState.launchMultiplePermissionRequest() } } else null, - description = "Required to communicate with AirPods", + description = stringResource(R.string.bluetooth_permission_description), orientation = ListItemOrientation.Vertical, leadingContent = { Box( @@ -165,7 +167,7 @@ fun PermissionsPage( ) { Icon( imageVector = MaterialIcons.bluetooth, - contentDescription = "bluetooth", + contentDescription = stringResource(R.string.bluetooth), modifier = Modifier.size(24.dp), tint = animatedBluetoothIconColor ) @@ -173,7 +175,7 @@ fun PermissionsPage( }, ) } - StyledList(title = "Optional Permissions") { + StyledList(title = stringResource(R.string.optional_permissions)) { val animatedNotificationsIconColor by animateColorAsState( if (notificationPermissionState.status.isGranted) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface ) @@ -186,14 +188,14 @@ fun PermissionsPage( ) StyledListItem( - name = "Notifications", + name = stringResource(R.string.notifications), onClick = if (!notificationPermissionState.status.isGranted) { { grantingAll = false notificationPermissionState.launchPermissionRequest() } } else null, - description = "Show battery status", + description = stringResource(R.string.notifications_permission_description), orientation = ListItemOrientation.Vertical, leadingContent = { Box( @@ -208,7 +210,7 @@ fun PermissionsPage( ) { Icon( imageVector = MaterialIcons.notifications, - contentDescription = "notifications", + contentDescription = stringResource(R.string.notifications), modifier = Modifier.size(24.dp), tint = animatedNotificationsIconColor ) @@ -216,14 +218,14 @@ fun PermissionsPage( }, ) StyledListItem( - name = "Phone", + name = stringResource(R.string.phone), onClick = if (!phonePermissionState.allPermissionsGranted) { { grantingAll = false phonePermissionState.launchMultiplePermissionRequest() } } else null, - description = "Respond to phone calls with head gestures", + description = stringResource(R.string.phone_permission_description), orientation = ListItemOrientation.Vertical, leadingContent = { Box( @@ -238,7 +240,7 @@ fun PermissionsPage( ) { Icon( imageVector = MaterialIcons.call, - contentDescription = "bluetooth", + contentDescription = stringResource(R.string.phone), modifier = Modifier.size(24.dp), tint = animatedPhoneIconColor ) @@ -251,7 +253,7 @@ fun PermissionsPage( val animatedOverlayContainerColor by animateColorAsState(if (canDrawOverlays.value) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest) StyledListItem( - name = "Display over other apps", + name = stringResource(R.string.display_over_other_apps), onClick = if (!canDrawOverlays.value) { { grantingAll = false @@ -262,7 +264,7 @@ fun PermissionsPage( context.startActivity(intent) } } else null, - description = "Show popups when AirPods are nearby or audio switches to them.", + description = stringResource(R.string.overlay_permission_description), orientation = ListItemOrientation.Vertical, leadingContent = { Box( @@ -277,7 +279,7 @@ fun PermissionsPage( ) { Icon( imageVector = MaterialIcons.stack, - contentDescription = "bluetooth", + contentDescription = stringResource(R.string.display_over_other_apps), modifier = Modifier.size(24.dp), tint = animatedOverlayIconColor ) @@ -300,7 +302,7 @@ fun PermissionsPage( ) { Icon( Icons.AutoMirrored.Default.ArrowBack, - contentDescription = "backward", + contentDescription = stringResource(R.string.back), modifier = Modifier.size(IconButtonDefaults.mediumIconSize), ) } @@ -319,7 +321,7 @@ fun PermissionsPage( enabled = !bluetoothPermissionsState.allPermissionsGranted || !notificationPermissionState.status.isGranted || !phonePermissionState.allPermissionsGranted || !canDrawOverlays.value ) { Text( - text = "Grant all", + text = stringResource(R.string.grant_all), style = MaterialTheme.typography.labelMedium ) } @@ -335,7 +337,7 @@ fun PermissionsPage( ) { Icon( Icons.AutoMirrored.Default.ArrowForward, - contentDescription = "forward", + contentDescription = stringResource(R.string.navigate_forward), modifier = Modifier.size(IconButtonDefaults.mediumIconSize), ) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/PrivacyPolicyPage.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/PrivacyPolicyPage.kt index 23eaa8377..a0c1e693b 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/PrivacyPolicyPage.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/onboarding/PrivacyPolicyPage.kt @@ -41,73 +41,73 @@ fun PrivacyPolicyPage( ) { Spacer(modifier = Modifier.height(24.dp)) Text( - text = "Last updated: 20 June 2026", + text = stringResource(R.string.privacy_last_updated), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) Text( - text = "Overview", + text = stringResource(R.string.privacy_overview), style = MaterialTheme.typography.titleLarge ) Text( - text = "LibrePods does not collect, store, sell, or share personal information for advertising, analytics, tracking, or profiling purposes. The app does not include analytics, crash reporting, telemetry, advertising SDKs, or tracking services.", + text = stringResource(R.string.privacy_data_practices), style = MaterialTheme.typography.bodyMedium ) Text( - text = "All information remains on your device unless you explicitly choose to contact me, create a GitHub issue from the app, or make a purchase or sponsorship through a third-party platform.", + text = stringResource(R.string.privacy_local_information), style = MaterialTheme.typography.bodyMedium ) Text( - text = "Third Party Services", + text = stringResource(R.string.privacy_third_party_services), style = MaterialTheme.typography.titleLarge ) Text( - text = "LibrePods provides several ways to contact me, including email, Discord, and GitHub Issues.", + text = stringResource(R.string.privacy_contact_methods), style = MaterialTheme.typography.bodyMedium ) Text( - text = "Email", + text = stringResource(R.string.email), style = MaterialTheme.typography.titleMedium ) Text( - text = "If you contact me by email, I receive your email address and any information you choose to include in your message. When using the contact form within LibrePods, your email client will open with a pre-filled email address, the subject line and body that you fill out. The body will also include LibrePods version information and device information to help with troubleshooting.", + text = stringResource(R.string.privacy_email_information), style = MaterialTheme.typography.bodyMedium ) Text( - text = "You can edit or remove any of this information before sending the email.", + text = stringResource(R.string.privacy_email_editing), style = MaterialTheme.typography.bodyMedium ) Text( - text = "Discord", + text = stringResource(R.string.discord), style = MaterialTheme.typography.titleMedium ) Text( - text = "The app provides a link to the LibrePods Discord server. If you choose to join the Discord server, you will be subject to Discord's privacy policy.", + text = stringResource(R.string.privacy_discord_policy), style = MaterialTheme.typography.bodyMedium ) Text( - text = "I do not receive any information about you from Discord other than what is publicly visible in the Discord server, such as your username, joining date, common servers, and any messages or content you post in the server, unless you choose to share it with me in the Discord server.", + text = stringResource(R.string.privacy_discord_information), style = MaterialTheme.typography.bodyMedium ) Text( - text = "GitHub Issues", + text = stringResource(R.string.github_issues), style = MaterialTheme.typography.titleMedium ) Text( - text = "When creating a GitHub issue through LibrePods, the app will pre-fill the issue form with:", + text = stringResource(R.string.privacy_github_issue_prefill), style = MaterialTheme.typography.bodyMedium ) @@ -116,68 +116,68 @@ fun PrivacyPolicyPage( modifier = Modifier.padding(start = 8.dp) ) { Text( - "• LibrePods version name and version code", + stringResource(R.string.privacy_bullet_1), style = MaterialTheme.typography.bodyMedium ) Text( - text = "• Device manufacturer and model", + text = stringResource(R.string.privacy_bullet_2), style = MaterialTheme.typography.bodyMedium ) Text( - text = "• Android build information", + text = stringResource(R.string.privacy_bullet_3), style = MaterialTheme.typography.bodyMedium ) Text( - text = "• Installation source (Google Play or GitHub)", + text = stringResource(R.string.privacy_bullet_4), style = MaterialTheme.typography.bodyMedium ) } Text( - text = "This information helps diagnose bugs and provide support. No information is sent automatically. The information is only submitted if you choose to create the GitHub issue.", + text = stringResource(R.string.privacy_github_issue_submission), style = MaterialTheme.typography.bodyMedium ) Text( - text = "Payments", style = MaterialTheme.typography.titleLarge + text = stringResource(R.string.privacy_payments), style = MaterialTheme.typography.titleLarge ) if (BuildConfig.PLAY_BUILD) { Text( - text = "Google Play", style = MaterialTheme.typography.titleMedium + text = stringResource(R.string.privacy_google_play), style = MaterialTheme.typography.titleMedium ) Text( - text = "When using the version available on Google Play, purchases are processed by Google Play.", + text = stringResource(R.string.privacy_google_play_purchases), style = MaterialTheme.typography.bodyMedium ) Text( - text = "LibrePods verifies the purchase with Google Play on-device, not with a remote server that I control. I do not receive any information about you or your purchase from Google Play. Payment processing is handled entirely by Google Play, and I do not have access to any of your payment information.", + text = stringResource(R.string.privacy_google_play_privacy), style = MaterialTheme.typography.bodyMedium ) } else { Text( - text = "GitHub Sponsors", style = MaterialTheme.typography.titleMedium + text = stringResource(R.string.privacy_github_sponsors), style = MaterialTheme.typography.titleMedium ) Text( - text = "When using the FOSS version available on GitHub, the upgrade button links to GitHub Sponsors. If you choose to sponsor LibrePods, your sponsorship is processed by GitHub.", + text = stringResource(R.string.privacy_github_sponsors_purchases), style = MaterialTheme.typography.bodyMedium ) Text( - text = "Your username and country/region are shared with me when you sponsor LibrePods. Depending on your GitHub Sponsors privacy settings, I may also receive your email address.", + text = stringResource(R.string.privacy_github_sponsors_information), style = MaterialTheme.typography.bodyMedium ) } Text( - text = "Contact", style = MaterialTheme.typography.titleLarge + text = stringResource(R.string.contact), style = MaterialTheme.typography.titleLarge ) Text( - text = "If you have questions about this privacy policy, please contact me via email at privacy@kavish.xyz.", + text = stringResource(R.string.privacy_contact_email), style = MaterialTheme.typography.bodyMedium ) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt index 0b43370e0..9cb60c1e8 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt @@ -18,6 +18,7 @@ package me.kavishdevar.librepods.presentation.viewmodel +import me.kavishdevar.librepods.R import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -775,7 +776,7 @@ class AirPodsViewModel( service.disconnectAirPods() if (appContext.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") != PackageManager.PERMISSION_GRANTED) { Toast.makeText( - appContext, "App has disconnected, disconnect from Android Settings.", + appContext, appContext.getString(R.string.app_disconnected_open_android_settings), Toast.LENGTH_LONG ).show() } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/PurchaseViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/PurchaseViewModel.kt index b9e5235ec..8cf744eac 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/PurchaseViewModel.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/PurchaseViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import me.kavishdevar.librepods.billing.BillingManager +import me.kavishdevar.librepods.billing.PRICE_LOADING data class PurchaseUiState( val isPremium: Boolean = false, @@ -17,7 +18,7 @@ data class PurchaseUiState( ) class PurchaseViewModel(application: Application) : AndroidViewModel(application) { - private val _uiState = MutableStateFlow(PurchaseUiState()) + private val _uiState = MutableStateFlow(PurchaseUiState(price = PRICE_LOADING)) val uiState = _uiState.asStateFlow() init { 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 8a7080163..34e72de16 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 @@ -215,7 +215,7 @@ class AirPodsQSService : TileService() { } else { tile.state = Tile.STATE_UNAVAILABLE tile.label = "AirPods" - tile.subtitle = "Disconnected" + tile.subtitle = getString(R.string.takeover_disconnected) tile.icon = Icon.createWithResource(this, R.drawable.airpods) } @@ -254,11 +254,11 @@ 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" - else -> "Unknown" + 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 -> getString(R.string.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 0cf08c11d..b6be4880b 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 @@ -1735,22 +1735,22 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList fun startForegroundNotification() { val disconnectedNotificationChannel = NotificationChannel( "background_service_status", - "Background Service Status", + getString(R.string.background_service_channel), NotificationManager.IMPORTANCE_NONE ) val connectedNotificationChannel = NotificationChannel( "airpods_connection_status", - "AirPods Connection Status", + getString(R.string.airpods_connection_channel), NotificationManager.IMPORTANCE_LOW, ) val socketFailureChannel = NotificationChannel( "socket_connection_failure", - "AirPods BluetoothConnectionManager.aacpSocket? Connection Issues", + getString(R.string.airpods_connection_issues_channel), NotificationManager.IMPORTANCE_HIGH ).apply { - description = "Notifications about problems connecting to AirPods protocol" + description = getString(R.string.airpods_connection_issues_channel_description) enableLights(true) lightColor = Color.RED enableVibration(true) @@ -1774,8 +1774,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.") + .setSmallIcon(R.drawable.airpods).setContentTitle(getString(R.string.background_service_running)) + .setContentText(getString(R.string.background_service_notification)) .setContentIntent(pendingIntentNotifDisable).setCategory(Notification.CATEGORY_SERVICE) .setPriority(NotificationCompat.PRIORITY_LOW).setOngoing(true).build() @@ -1808,10 +1808,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").setStyle( + .setSmallIcon(R.drawable.airpods).setContentTitle(getString(R.string.airpods_connection_issue)) + .setContentText(getString(R.string.airpods_l2cap_connection_failed)).setStyle( NotificationCompat.BigTextStyle().bigText( - "Your AirPods are connected via Bluetooth, but LibrePods couldn't connect to AirPods using L2CAP. Error: $errorMessage" + getString(R.string.airpods_l2cap_connection_failure_detail, errorMessage) ) ).setContentIntent(pendingIntent).setCategory(Notification.CATEGORY_ERROR) .setPriority(NotificationCompat.PRIORITY_HIGH).setAutoCancel(true).build() @@ -2059,7 +2059,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList """${ batteryList?.find { it.component == BatteryComponent.LEFT }?.let { if (it.status != BatteryStatus.DISCONNECTED) { - "L: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" + getString(R.string.battery_component_status, getString(R.string.battery_left_short), if (it.status == BatteryStatus.CHARGING) "⚡" else "", it.level) } else { "" } @@ -2067,7 +2067,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } ${ batteryList?.find { it.component == BatteryComponent.RIGHT }?.let { if (it.status != BatteryStatus.DISCONNECTED) { - "R: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" + getString(R.string.battery_component_status, getString(R.string.battery_right_short), if (it.status == BatteryStatus.CHARGING) "⚡" else "", it.level) } else { "" } @@ -2075,7 +2075,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } ${ batteryList?.find { it.component == BatteryComponent.CASE }?.let { if (it.status != BatteryStatus.DISCONNECTED) { - "Case: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" + getString(R.string.battery_component_status, getString(R.string.battery_case_short), if (it.status == BatteryStatus.CHARGING) "⚡" else "", it.level) } else { "" } @@ -2153,10 +2153,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList answerCallMethod.invoke(telephonyInterface) } - sendToast("Call answered via head gesture") + sendToast(getString(R.string.call_answered_by_head_gesture)) } catch (e: Exception) { e.printStackTrace() - sendToast("Failed to answer call: ${e.message}") + sendToast(getString(R.string.call_answer_failed, e.message)) } finally { islandWindow?.close() } @@ -2179,10 +2179,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList endCallMethod.invoke(telephonyInterface) } - sendToast("Call rejected via head gesture") + sendToast(getString(R.string.call_rejected_by_head_gesture)) } catch (e: Exception) { e.printStackTrace() - sendToast("Failed to reject call: ${e.message}") + sendToast(getString(R.string.call_reject_failed, e.message)) } finally { islandWindow?.close() } @@ -2717,10 +2717,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList ) if (manual) { sendToast( - "Couldn't connect to socket: ${e.localizedMessage}" + getString(R.string.socket_connection_failed, e.localizedMessage) ) } else { - showSocketConnectionFailureNotification("Couldn't connect to socket: ${e.localizedMessage}") + showSocketConnectionFailureNotification(getString(R.string.socket_connection_failed, e.localizedMessage)) } return@withTimeout // throw e // lol how did i not catch this before... gonna comment this line instead of removing to preserve history @@ -2731,10 +2731,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList Log.d(TAG, " socket not connected") if (manual) { sendToast( - "Couldn't connect to socket: timeout." + getString(R.string.socket_connection_timeout) ) } else { - showSocketConnectionFailureNotification("Couldn't connect to socket: Timeout") + showSocketConnectionFailureNotification(getString(R.string.socket_connection_timeout_reason)) } return } diff --git a/android/app/src/main/res/layout/battery_widget.xml b/android/app/src/main/res/layout/battery_widget.xml index df54f5d45..0209463f0 100644 --- a/android/app/src/main/res/layout/battery_widget.xml +++ b/android/app/src/main/res/layout/battery_widget.xml @@ -77,7 +77,7 @@ android:fontFamily="@font/sf_pro" android:gravity="center" android:textFontWeight="300" - android:text="Phone" + android:text="@string/phone" tools:ignore="HardcodedText" /> @@ -139,7 +139,7 @@ android:fontFamily="@font/sf_pro" android:gravity="center" android:textFontWeight="300" - android:text="Left" + android:text="@string/left" tools:ignore="HardcodedText" /> @@ -200,7 +200,7 @@ android:textColor="@color/white" android:fontFamily="@font/sf_pro" android:gravity="center" - android:text="Right" + android:text="@string/right" tools:ignore="HardcodedText" /> @@ -261,7 +261,7 @@ android:gravity="center" android:fontFamily="@font/sf_pro" android:textFontWeight="300" - android:text="Case" + android:text="@string/case_alt" tools:ignore="HardcodedText" /> diff --git a/android/app/src/main/res/layout/popup_window.xml b/android/app/src/main/res/layout/popup_window.xml index f00068db0..eb012d288 100644 --- a/android/app/src/main/res/layout/popup_window.xml +++ b/android/app/src/main/res/layout/popup_window.xml @@ -38,7 +38,7 @@ android:layout_marginTop="12dp" android:layout_marginEnd="24dp" android:background="@drawable/popup_button_shape" - android:contentDescription="Close Button" + android:contentDescription="@string/close_button" android:src="@drawable/close" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index c2c3f7a38..7097a6cae 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -291,4 +291,127 @@ Privacy Policy I agree Compatibility check bypassed. Are you sure your device is compatible? + Last updated: 20 June 2026 + Overview + LibrePods does not collect, store, sell, or share personal information for advertising, analytics, tracking, or profiling purposes. The app does not include analytics, crash reporting, telemetry, advertising SDKs, or tracking services. + All information remains on your device unless you explicitly choose to contact me, create a GitHub issue from the app, or make a purchase or sponsorship through a third-party platform. + Third Party Services + LibrePods provides several ways to contact me, including email, Discord, and GitHub Issues. + If you contact me by email, I receive your email address and any information you choose to include in your message. When using the contact form within LibrePods, your email client will open with a pre-filled email address, the subject line and body that you fill out. The body will also include LibrePods version information and device information to help with troubleshooting. + You can edit or remove any of this information before sending the email. + The app provides a link to the LibrePods Discord server. If you choose to join the Discord server, you will be subject to Discord\'s privacy policy. + I do not receive any information about you from Discord other than what is publicly visible in the Discord server, such as your username, joining date, common servers, and any messages or content you post in the server, unless you choose to share it with me in the Discord server. + When creating a GitHub issue through LibrePods, the app will pre-fill the issue form with: + • LibrePods version name and version code + • Device manufacturer and model + • Android build information + • Installation source (Google Play or GitHub) + This information helps diagnose bugs and provide support. No information is sent automatically. The information is only submitted if you choose to create the GitHub issue. + Payments + Google Play + When using the version available on Google Play, purchases are processed by Google Play. + LibrePods verifies the purchase with Google Play on-device, not with a remote server that I control. I do not receive any information about you or your purchase from Google Play. Payment processing is handled entirely by Google Play, and I do not have access to any of your payment information. + GitHub Sponsors + When using the FOSS version available on GitHub, the upgrade button links to GitHub Sponsors. If you choose to sponsor LibrePods, your sponsorship is processed by GitHub. + Your username and country/region are shared with me when you sponsor LibrePods. Depending on your GitHub Sponsors privacy settings, I may also receive your email address. + If you have questions about this privacy policy, please contact me via email at privacy@kavish.xyz. + Welcome to + Forward + Required Permissions + Required to communicate with AirPods + Optional Permissions + Show battery status + Respond to phone calls with head gestures + Display over other apps + Show popups when AirPods are nearby or audio switches to them. + Grant all + Test Head Gestures + \"Yes\" gesture detected. + \"No\" gesture detected. + Velocity + Horizontal + Vertical + Enable Hearing Aid + Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode. + Enable + Cancel + Loading... + Device icon + Unknown + ANC Mode + Close button + Play or pause + App has disconnected, disconnect from Android Settings. + Loading price... + Price unavailable + LibrePods Play billing error + Please enter your GitHub username to restore your premium access:\n\nGitHub username: + Bluetooth + Notifications + Log saved successfully + Failed to save log: %1$s + 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. + Delete All + Delete + Open Xposed Settings + Log saved: %1$s + Error collecting logs: %1$s + Continue + Preparing... + Collecting logs... + Log collection stopped + Stop Collection + Share log file + Share + Save + Done + Delete Log File + Are you sure you want to delete this log file? This action cannot be undone. + Log file deleted + Failed to delete log file + Delete All Logs + Failed to delete log files + Error loading log content: %1$s + Log Content + Phone details: + App details: + Low + Mid + High + Background Service Status + AirPods Connection Status + AirPods Connection Issues + Notifications about problems connecting to AirPods protocol + 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. Error: %1$s + %1$s: %2$s %3$d%% + L + R + Case + Call answered via head gesture + Failed to answer call: %1$s + Call rejected via head gesture + Failed to reject call: %1$s + Couldn\'t connect to socket: %1$s + Couldn\'t connect to socket: timeout. + Couldn\'t connect to socket: Timeout + + Total logs: %d + Total logs: %d + + + Are you sure you want to delete all log files? This action cannot be undone and will remove %d log file. + Are you sure you want to delete all log files? This action cannot be undone and will remove %d log files. + + + Deleted %d log file + Deleted %d log files +