nordiccalendar

A simple Material 3 native calendar app fitting my personal requirements
git clone git://archive.git.mtrnord.blog/MTRNord/nordiccalendar.git
Log | Files | Refs | README | LICENSE

commit d17bbae470335241b63bdf1a7392ae9f9322aa23
parent e81bce5744ed9b3155e17a58a1d665015bc70b52
Author: MTRNord <MTRNord@users.noreply.github.com>
Date:   Tue, 29 Jul 2025 18:56:42 +0200

Prevent leaking urls to nominatim and allow custom nominatim servers

Diffstat:
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt | 23+----------------------
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt | 68+++++++++++++++++++++++++++-----------------------------------------
Aapp/src/main/java/space/midnightthoughts/nordiccalendar/screens/SettingsView.kt | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt | 4+++-
Aapp/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SettingsViewModel.kt | 29+++++++++++++++++++++++++++++
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SidebarDrawerViewModel.kt | 2+-
Mapp/src/main/res/values-en/strings.xml | 3+++
Mapp/src/main/res/values/strings.xml | 3+++
Mgradle.properties | 2+-
9 files changed, 150 insertions(+), 66 deletions(-)

diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt @@ -58,6 +58,7 @@ import space.midnightthoughts.nordiccalendar.onboarding.OnBoardItem import space.midnightthoughts.nordiccalendar.onboarding.onBoardingData import space.midnightthoughts.nordiccalendar.screens.CalendarScreen import space.midnightthoughts.nordiccalendar.screens.EventDetailsView +import space.midnightthoughts.nordiccalendar.screens.SettingsView import space.midnightthoughts.nordiccalendar.ui.theme.NordicCalendarTheme import space.midnightthoughts.nordiccalendar.util.OnboardingPrefs import space.midnightthoughts.nordiccalendar.viewmodels.CalendarViewModel @@ -267,28 +268,6 @@ fun CalendarView( } } - -@Composable -fun SettingsView(navController: NavHostController) { - AppScaffold( - title = stringResource(R.string.settings), - selectedDestination = "settings", - navController = navController, - - ) { innerPadding -> - Column( - modifier = innerPadding - .fillMaxSize() - ) { - Text( - stringResource(R.string.settings), - modifier = Modifier.padding(16.dp) - ) - // Weitere Einstellungen hier - } - } -} - @Composable fun AboutView(navController: NavHostController) { diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt @@ -1,5 +1,6 @@ package space.midnightthoughts.nordiccalendar.screens +import android.util.Patterns import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement @@ -100,10 +101,14 @@ fun EventDetailsView( }.collectAsState() val locationText = event.value?.location?.trim() + // Prevent leaking the url to third party services + val isValidUrlInLocation = remember(locationText) { + Patterns.WEB_URL.matcher(locationText ?: "").matches() + } val locationPosition by viewModel.locationPosition.collectAsState() val appLocaleString = appLocale.language LaunchedEffect(locationText, appLocaleString) { - if (!locationText.isNullOrEmpty()) { + if (!locationText.isNullOrEmpty() && !isValidUrlInLocation) { viewModel.resolveLocation(locationText, appLocaleString) } } @@ -236,45 +241,18 @@ fun EventDetailsView( color = MaterialTheme.colorScheme.outlineVariant ) Spacer(modifier = Modifier.height(16.dp)) - if (!locationText.isNullOrEmpty()) { - if (locationPosition != null) { - Column { - Text( - text = stringResource(R.string.location), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.height(8.dp)) - LocationMap( - coordinate = locationPosition!!, - boundingBox = boundingBox.value - ) - } - } else { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = stringResource(R.string.location), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.width(24.dp)) - Text( - text = AnnotatedString.rememberAutoLinkText( - if (event.value?.location?.trim().isNullOrEmpty()) { - stringResource(R.string.no_location_available) - } else { - event.value?.location ?: "" - } - ), - - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + if (locationPosition != null && !isValidUrlInLocation) { + Column { + Text( + text = stringResource(R.string.location), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + LocationMap( + coordinate = locationPosition!!, + boundingBox = boundingBox.value + ) } } else { Row( @@ -289,11 +267,19 @@ fun EventDetailsView( ) Spacer(modifier = Modifier.width(24.dp)) Text( - text = stringResource(R.string.no_location_available), + text = AnnotatedString.rememberAutoLinkText( + if (event.value?.location?.trim().isNullOrEmpty()) { + stringResource(R.string.no_location_available) + } else { + event.value?.location ?: "" + } + ), + style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, ) } + } // TODO: Invitees and attendees and alerts diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/SettingsView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/SettingsView.kt @@ -0,0 +1,81 @@ +package space.midnightthoughts.nordiccalendar.screens + +import android.util.Patterns +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import space.midnightthoughts.nordiccalendar.R +import space.midnightthoughts.nordiccalendar.components.AppScaffold +import space.midnightthoughts.nordiccalendar.viewmodels.SettingsViewModel + +@Composable +fun SettingsView(navController: NavHostController) { + val viewModel: SettingsViewModel = hiltViewModel() + val nominatimUrl = remember(viewModel) { + viewModel.nominatimUrl + }.collectAsState() + val textFieldValue: MutableState<String> = remember { mutableStateOf(nominatimUrl.value) } + + val isValidUrl = remember(textFieldValue) { + Patterns.WEB_URL.matcher(textFieldValue.value).matches() + } + AppScaffold( + title = stringResource(R.string.settings), + selectedDestination = "settings", + navController = navController, + + ) { innerPadding -> + Column( + modifier = innerPadding + .padding(16.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = textFieldValue.value, + onValueChange = { value: String -> + textFieldValue.value = value + }, + label = { Text(stringResource(R.string.nominatim_url_label)) }, + singleLine = true, + supportingText = { Text(stringResource(R.string.nominatim_url_hint)) }, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrectEnabled = false, + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Done + ), + isError = !isValidUrl && textFieldValue.value.isNotBlank(), + modifier = Modifier.fillMaxWidth() + ) + Button( + onClick = { viewModel.setNominatimUrl(textFieldValue.value) }, + enabled = textFieldValue.value.isNotBlank() && textFieldValue != nominatimUrl && isValidUrl, + modifier = Modifier.align(Alignment.End) + ) { + Text(stringResource(R.string.save)) + } + } + } +} +\ No newline at end of file diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt @@ -97,7 +97,9 @@ class EventDetailsViewModel @Inject constructor( fun resolveLocation(address: String, locale: String = "de") { viewModelScope.launch { try { - val url = "https://nominatim.openstreetmap.org/search" + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val url = + prefs.getString("nominatim_url", "https://nominatim.openstreetmap.org/search")!! val response: List<NominatimResult> = ktorClient.get(url) { url { parameters.append("q", address) diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SettingsViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SettingsViewModel.kt @@ -0,0 +1,29 @@ +package space.midnightthoughts.nordiccalendar.viewmodels + +import android.content.Context +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + @param:ApplicationContext private val context: Context +) : ViewModel() { + private val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + private val _nominatimUrl = MutableStateFlow( + prefs.getString( + "nominatim_url", + "https://nominatim.openstreetmap.org/search" + )!! + ) + val nominatimUrl: StateFlow<String> = _nominatimUrl.asStateFlow() + + fun setNominatimUrl(url: String) { + prefs.edit().putString("nominatim_url", url).apply() + _nominatimUrl.value = url + } +} diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SidebarDrawerViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SidebarDrawerViewModel.kt @@ -14,7 +14,7 @@ import javax.inject.Inject @HiltViewModel class SidebarDrawerViewModel @Inject constructor( - @ApplicationContext private val context: Context, + @param:ApplicationContext private val context: Context, private val repository: CalendarRepository ) : ViewModel() { val calendars = repository.calendarsFlow.stateIn( diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml @@ -38,5 +38,8 @@ <string name="event_not_found">Event not found or loading…</string> <string name="event_time_range">From %1$s to %2$s</string> <string name="event_date_range">%1$s until %2$s</string> + <string name="nominatim_url_hint">Example: https://nominatim.openstreetmap.org/search</string> + <string name="nominatim_url_label">OpenStreetMap Nominatim URL</string> + <string name="save">Save</string> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml @@ -38,5 +38,8 @@ <string name="event_not_found">Event nicht gefunden oder wird geladen…</string> <string name="event_time_range">Von %1$s bis %2$s</string> <string name="event_date_range">%1$s bis %2$s</string> + <string name="nominatim_url_hint">Beispiel: https://nominatim.openstreetmap.org/search</string> + <string name="nominatim_url_label">OpenStreetMap Nominatim URL</string> + <string name="save">Speichern</string> </resources> diff --git a/gradle.properties b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx6g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g +org.gradle.jvmargs=-Xmx6g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. For more details, visit # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects