From f585372594ff1e496fbef2193a26a159695cdbf9 Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Thu, 30 Jul 2026 17:15:02 +0200 Subject: [PATCH] Added cleartext traffic support for custom TLE URLs #227 --- app/src/main/AndroidManifest.xml | 1 + .../main/res/xml/network_security_config.xml | 4 ++ .../feature/settings/SettingsDialog.kt | 24 +++++++++- .../feature/settings/SettingsScreen.kt | 46 +++++++++++++++++-- 4 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cd9649c9..343282c7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round"> + + + diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt index fbeccea3..c0897b4d 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt @@ -120,6 +120,7 @@ private fun TransceiversDialogPreview() { useCustomTransceivers = true, tleUrl = "https://example.com/tle.txt", transceiversUrl = "https://example.com/tx.json", + requestCustomSourcesPermission = { onGranted, _ -> onGranted() }, onImportTle = {}, onImportTransceivers = {}, onDismiss = {}, @@ -134,6 +135,7 @@ fun DataSourcesDialog( useCustomTransceivers: Boolean, tleUrl: String, transceiversUrl: String, + requestCustomSourcesPermission: (onGranted: () -> Unit, onDenied: () -> Unit) -> Unit, onImportTle: () -> Unit, onImportTransceivers: () -> Unit, onDismiss: () -> Unit, @@ -182,7 +184,16 @@ fun DataSourcesDialog( Text(text = stringResource(id = R.string.prefs_data_sources_tle_switch)) Switch( checked = isEnabledCustomTle.value, - onCheckedChange = { isEnabledCustomTle.value = it } + onCheckedChange = { enabled -> + if (!enabled) { + isEnabledCustomTle.value = false + } else { + requestCustomSourcesPermission( + { isEnabledCustomTle.value = true }, + { isEnabledCustomTle.value = false } + ) + } + } ) } OutlinedTextField( @@ -202,7 +213,16 @@ fun DataSourcesDialog( Text(text = stringResource(id = R.string.prefs_data_sources_transceivers_switch)) Switch( checked = isEnabledCustomTransceivers.value, - onCheckedChange = { isEnabledCustomTransceivers.value = it } + onCheckedChange = { enabled -> + if (!enabled) { + isEnabledCustomTransceivers.value = false + } else { + requestCustomSourcesPermission( + { isEnabledCustomTransceivers.value = true }, + { isEnabledCustomTransceivers.value = false } + ) + } + } ) } OutlinedTextField( diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt index 9d7a1263..e4c9e2b0 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt @@ -89,10 +89,22 @@ fun SettingsDestination() { @Composable private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> Unit) { val dialogs = rememberDialogVisibility() + val pendingCustomSourcesGrant = remember { mutableStateOf<(() -> Unit)?>(null) } + val pendingCustomSourcesDeny = remember { mutableStateOf<(() -> Unit)?>(null) } val permissions = rememberSettingsPermissions( sendAction = onAction, onBluetoothGranted = { dialogs.bluetooth = true }, - onNetworkGranted = { dialogs.network = true } + onNetworkGranted = { dialogs.network = true }, + onCustomSourcesPermissionGranted = { + pendingCustomSourcesGrant.value?.invoke() + pendingCustomSourcesGrant.value = null + pendingCustomSourcesDeny.value = null + }, + onCustomSourcesPermissionDenied = { + pendingCustomSourcesDeny.value?.invoke() + pendingCustomSourcesGrant.value = null + pendingCustomSourcesDeny.value = null + } ) // Dialogs @@ -117,6 +129,11 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> useCustomTransceivers = uiState.dataSourcesSettings.useCustomTransceivers, tleUrl = uiState.dataSourcesSettings.tleUrl, transceiversUrl = uiState.dataSourcesSettings.transceiversUrl, + requestCustomSourcesPermission = { onGranted, onDenied -> + pendingCustomSourcesGrant.value = onGranted + pendingCustomSourcesDeny.value = onDenied + permissions.launchCustomSourcesPermission() + }, onImportTle = { permissions.launchTleImport(); dialogs.dataSources = false }, onImportTransceivers = { permissions.launchTransceiverImport(); dialogs.dataSources = false }, onDismiss = { dialogs.dataSources = false }, @@ -129,7 +146,9 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> transceiversUrl = if (!useCustomTransceivers || transceiversUrl.isNotBlank()) transceiversUrl else current.transceiversUrl ) if (newSettings != current) onAction(SettingsAction.UpdateDataSources(newSettings)) - if (useCustomTle || useCustomTransceivers) onAction(SettingsAction.UpdateFromWeb) + if (newSettings.useCustomTLE || newSettings.useCustomTransceivers) { + onAction(SettingsAction.UpdateFromWeb) + } } ) } @@ -653,14 +672,17 @@ private class SettingsPermissions( val launchTleImport: () -> Unit, val launchTransceiverImport: () -> Unit, val launchBluetooth: () -> Unit, - val launchNetwork: () -> Unit + val launchNetwork: () -> Unit, + val launchCustomSourcesPermission: () -> Unit ) @Composable private fun rememberSettingsPermissions( sendAction: (SettingsAction) -> Unit, onBluetoothGranted: () -> Unit, - onNetworkGranted: () -> Unit + onNetworkGranted: () -> Unit, + onCustomSourcesPermissionGranted: () -> Unit, + onCustomSourcesPermissionDenied: () -> Unit ): SettingsPermissions { val locationError = stringResource(R.string.prefs_loc_gps_error) val locationRequest = rememberLauncherForActivityResult( @@ -697,6 +719,15 @@ private fun rememberSettingsPermissions( else sendAction(SettingsAction.ShowToast(networkError)) } + val customSourcesRequest = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + if (granted) { + onCustomSourcesPermissionGranted() + } else { + onCustomSourcesPermissionDenied() + sendAction(SettingsAction.ShowToast(networkError)) + } + } + return remember { SettingsPermissions( launchLocation = { @@ -713,6 +744,13 @@ private fun rememberSettingsPermissions( } else { onNetworkGranted() } + }, + launchCustomSourcesPermission = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) { + customSourcesRequest.launch(Manifest.permission.ACCESS_LOCAL_NETWORK) + } else { + onCustomSourcesPermissionGranted() + } } ) }