Skip to content
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ android {
// Maintaining this list means that we can exclude translations that aren't complete yet
resourceConfigurations.addAll(listOf(
"arz",
"zh-rCN",
"da",
"de",
"el",
Expand All @@ -59,6 +60,7 @@ android {
"fi",
"fr",
"fr-rCA",
"hi",
"is",
"it",
"ja",
Expand All @@ -67,6 +69,7 @@ android {
"pl",
"pt",
"pt-rBR",
"ro",
"ru",
"sv",
"uk"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import android.text.Html
import android.util.Log
Expand Down Expand Up @@ -41,13 +42,15 @@ import kotlinx.coroutines.launch
import org.scottishtecharmy.soundscape.audio.AudioTour
import org.scottishtecharmy.soundscape.geoengine.utils.ResourceMapper
import org.scottishtecharmy.soundscape.geoengine.utils.geocoders.AndroidGeocoder
import org.scottishtecharmy.soundscape.geojsonparser.geojson.FeatureCollection
import org.scottishtecharmy.soundscape.screens.home.HomeRoutes
import org.scottishtecharmy.soundscape.screens.home.HomeScreen
import org.scottishtecharmy.soundscape.screens.home.Navigator
import org.scottishtecharmy.soundscape.services.SoundscapeService
import org.scottishtecharmy.soundscape.ui.theme.SoundscapeTheme
import org.scottishtecharmy.soundscape.utils.Analytics
import org.scottishtecharmy.soundscape.utils.LogcatHelper
import org.scottishtecharmy.soundscape.utils.findExtracts
import org.scottishtecharmy.soundscape.utils.getOfflineMapStorage
import org.scottishtecharmy.soundscape.utils.processMaps
import java.io.File
Expand Down Expand Up @@ -474,13 +477,25 @@ class MainActivity : AppCompatActivity() {
val bodyText = StringBuilder()

bodyText.append("-----------------------------<br/>")
bodyText.append(tableRow("Summary", subjectText))
bodyText.append(tableRow("Product", product))
bodyText.append(tableRow("Manufacturer", manufacturer))
talkBackDescription(bodyText, applicationContext)


bodyText.append(tableRow("AndroidGeocoder", AndroidGeocoder.enabled.toString()))

val extractPath = sharedPreferences.getString(SELECTED_STORAGE_KEY, SELECTED_STORAGE_DEFAULT)!!
val extractCollection = findExtracts(File(extractPath, Environment.DIRECTORY_DOWNLOADS).path)
if(extractCollection != null) {
for (extract in extractCollection.features) {
bodyText.append(tableRow
(
"Offline extract",
"${extract.properties?.get("name")}, ${extract.properties?.get("filename")}"
)
)
}
}
preferences.forEach { pref -> bodyText.append(tableRow(pref.key, pref.value.toString())) }
bodyText.append("-----------------------------<br/><br/>")

Expand Down Expand Up @@ -679,6 +694,8 @@ class MainActivity : AppCompatActivity() {
const val SELECTED_STORAGE_KEY = "SelectedStorage"
const val LAST_NEW_RELEASE_DEFAULT = ""
const val LAST_NEW_RELEASE_KEY = "LastNewRelease"
const val LANGUAGE_SUPPORTED_PROMPTED_DEFAULT = false
const val LANGUAGE_SUPPORTED_PROMPTED_KEY = "LanguageSupported"
const val GEOCODER_MODE_DEFAULT = "Auto"
const val GEOCODER_MODE_KEY = "GeocoderMode"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import kotlinx.coroutines.launch
import org.maplibre.android.maps.MapLibreMap.OnMapLongClickListener
import org.scottishtecharmy.soundscape.BuildConfig
import org.scottishtecharmy.soundscape.MainActivity
import org.scottishtecharmy.soundscape.MainActivity.Companion.LANGUAGE_SUPPORTED_PROMPTED_DEFAULT
import org.scottishtecharmy.soundscape.MainActivity.Companion.LANGUAGE_SUPPORTED_PROMPTED_KEY
import org.scottishtecharmy.soundscape.MainActivity.Companion.LAST_NEW_RELEASE_DEFAULT
import org.scottishtecharmy.soundscape.MainActivity.Companion.LAST_NEW_RELEASE_KEY
import org.scottishtecharmy.soundscape.MainActivity.Companion.SHOW_MAP_DEFAULT
Expand All @@ -65,6 +67,7 @@ import org.scottishtecharmy.soundscape.screens.markers_routes.components.Flexibl
import org.scottishtecharmy.soundscape.screens.talkbackHint
import org.scottishtecharmy.soundscape.services.RoutePlayerState
import org.scottishtecharmy.soundscape.ui.theme.SoundscapeTheme
import org.scottishtecharmy.soundscape.utils.getLanguageMismatch
import org.scottishtecharmy.soundscape.viewmodels.home.HomeState

@Composable
Expand Down Expand Up @@ -106,6 +109,13 @@ fun Home(
!= BuildConfig.VERSION_NAME.substringBeforeLast(".")
)
}
val phoneLanguage = remember { getLanguageMismatch(context) }
val languageMismatchDialog = remember {
mutableStateOf(
phoneLanguage != null &&
!sharedPreferences.getBoolean(LANGUAGE_SUPPORTED_PROMPTED_KEY, LANGUAGE_SUPPORTED_PROMPTED_DEFAULT)
)
}

// Memoize drawer callbacks to avoid recreating on every recomposition
val shareRecording = remember { { (context as MainActivity).shareRecording() } }
Expand Down Expand Up @@ -166,7 +176,11 @@ fun Home(
contentWindowInsets = WindowInsets(0, 0, 0, 0),
) { innerPadding ->

if(newReleaseDialog.value) {
// Prioritise the new language dialog over new release so that users can get
// the translated version of the new release dialog!
if(languageMismatchDialog.value && phoneLanguage != null) {
LanguageMismatchDialog(innerPadding, sharedPreferences, languageMismatchDialog, phoneLanguage)
} else if(newReleaseDialog.value) {
NewReleaseDialog(innerPadding, sharedPreferences, newReleaseDialog, toggleTutorial)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.scottishtecharmy.soundscape.screens.home.home

import android.content.SharedPreferences
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
import org.scottishtecharmy.soundscape.MainActivity.Companion.LANGUAGE_SUPPORTED_PROMPTED_KEY
import org.scottishtecharmy.soundscape.R
import org.scottishtecharmy.soundscape.screens.onboarding.language.Language

/**
* Dialog shown when the phone's language differs from the app's configured language
* and the phone language is supported by the app.
*/
@Composable
fun LanguageMismatchDialog(
innerPadding: PaddingValues,
sharedPreferences: SharedPreferences,
showDialog: MutableState<Boolean>,
phoneLanguage: Language,
) {
AlertDialog(
modifier = Modifier.padding(innerPadding),
title = {
Text(text = stringResource(R.string.language_mismatch_title))
},
text = {
Text(
text = stringResource(R.string.language_mismatch_message, phoneLanguage.name)
)
},
onDismissRequest = { },
confirmButton = {
TextButton(
onClick = {
sharedPreferences.edit(commit = true) {
putBoolean(LANGUAGE_SUPPORTED_PROMPTED_KEY, true)
}
showDialog.value = false
val list = LocaleListCompat.forLanguageTags(
"${phoneLanguage.code}-${phoneLanguage.region}"
)
AppCompatDelegate.setApplicationLocales(list)
}
) {
Text(text = stringResource(R.string.language_mismatch_switch, phoneLanguage.name))
}
},
dismissButton = {
TextButton(
onClick = {
sharedPreferences.edit(commit = true) {
putBoolean(LANGUAGE_SUPPORTED_PROMPTED_KEY, true)
}
showDialog.value = false
}
) {
Text(text = stringResource(R.string.language_mismatch_keep))
}
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import org.scottishtecharmy.soundscape.components.OnboardButton
import org.scottishtecharmy.soundscape.screens.onboarding.component.BoxWithGradientBackground
import org.scottishtecharmy.soundscape.ui.theme.SoundscapeTheme
import org.scottishtecharmy.soundscape.ui.theme.spacing
import org.scottishtecharmy.soundscape.utils.supportedLanguages

@Composable
fun LanguageScreen(
Expand Down Expand Up @@ -132,30 +133,7 @@ fun LanguageComposable(

// Data used by preview
data object MockLanguagePreviewData {
val languages = listOf(
Language("العربية المصرية", "arz", "EG"),
Language("Dansk", "da", "DK"),
Language("Deutsch", "de", "DE"),
Language("Ελληνικά", "el", "GR"),
Language("English", "en", "US"),
Language("English (UK)", "en", "GB"),
Language("Español", "es", "ES"),
Language("فارسی", "fa", "IR"),
Language("Suomi", "fi", "FI"),
Language("Français", "fr", "FR"),
Language("Français (Canada)", "fr", "CA"),
Language("Íslenska", "is", "IS"),
Language("Italiano", "it", "IT"),
Language("日本語", "ja", "JP"),
Language("Norsk", "nb", "NO"),
Language("Nederlands", "nl", "NL"),
Language("Polski", "pl", "PL"),
Language("Português (Brasil)", "pt", "BR"),
Language("Português (Portugal)", "pt", "PT"),
Language("Русский", "ru", "RU"),
Language("Svenska", "sv", "SE"),
Language("українська", "uk", "UK")
)
val languages = supportedLanguages
}

@Preview(device = "spec:parent=pixel_5,orientation=landscape")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.scottishtecharmy.soundscape.utils.supportedLanguages
import java.util.Locale
import javax.inject.Inject

Expand All @@ -16,7 +17,7 @@ class LanguageViewModel @Inject constructor(): ViewModel() {
private val _state : MutableStateFlow<LanguageUiState> = MutableStateFlow(LanguageUiState())
val state: StateFlow<LanguageUiState> = _state.asStateFlow()
init {
val allLanguages = getAllLanguages()
val allLanguages = supportedLanguages
_state.value = LanguageUiState(
supportedLanguages = allLanguages,
selectedLanguageIndex = allLanguages.indexOfLanguageMatchingDeviceLanguage()
Expand All @@ -28,39 +29,6 @@ class LanguageViewModel @Inject constructor(): ViewModel() {
var selectedLanguageIndex: Int = -1
)

private fun addIfSpeechSupports(allLanguages: MutableList<Language>, language: Language) {
allLanguages.add(language)
}

private fun getAllLanguages(): List<Language> {
val allLanguages = mutableListOf<Language>()

addIfSpeechSupports(allLanguages, Language("العربية المصرية", "arz", "EG"))
addIfSpeechSupports(allLanguages, Language("Dansk", "da", "DK"))
addIfSpeechSupports(allLanguages, Language("Deutsch", "de", "DE"))
addIfSpeechSupports(allLanguages, Language("Ελληνικά", "el", "GR"))
addIfSpeechSupports(allLanguages, Language("English", "en", "US"))
addIfSpeechSupports(allLanguages, Language("English (UK)", "en", "GB"))
addIfSpeechSupports(allLanguages, Language("Español", "es", "ES"))
addIfSpeechSupports(allLanguages, Language("فارسی", "fa", "IR"))
addIfSpeechSupports(allLanguages, Language("Suomi", "fi", "FI"))
addIfSpeechSupports(allLanguages, Language("Français (France)", "fr", "FR"))
addIfSpeechSupports(allLanguages, Language("Français (Canada)", "fr", "CA"))
addIfSpeechSupports(allLanguages, Language("Íslenska", "is", "IS"))
addIfSpeechSupports(allLanguages, Language("Italiano", "it", "IT"))
addIfSpeechSupports(allLanguages, Language("日本語", "ja", "JP"))
addIfSpeechSupports(allLanguages, Language("Norsk", "nb", "NO"))
addIfSpeechSupports(allLanguages, Language("Nederlands", "nl", "NL"))
addIfSpeechSupports(allLanguages, Language("Polski", "pl", "PL"))
addIfSpeechSupports(allLanguages, Language("Português (Portugal)", "pt", "PT"))
addIfSpeechSupports(allLanguages, Language("Português (Brasil)", "pt", "BR"))
addIfSpeechSupports(allLanguages, Language("Русский", "ru", "RU"))
addIfSpeechSupports(allLanguages, Language("Svenska", "sv", "SE"))
addIfSpeechSupports(allLanguages, Language("українська", "uk", "UK"))

return allLanguages
}

private fun List<Language>.indexOfLanguageMatchingDeviceLanguage(): Int {
val phoneLocale = Locale.getDefault()
val deviceLanguage = phoneLocale.language
Expand All @@ -83,7 +51,6 @@ class LanguageViewModel @Inject constructor(): ViewModel() {
val indexOfSelectedLanguage = _state.value.supportedLanguages.indexOf(selectedLanguage)
_state.value = _state.value.copy(selectedLanguageIndex = indexOfSelectedLanguage)


val list = LocaleListCompat.forLanguageTags("${selectedLanguage.code}-${selectedLanguage.region}")
AppCompatDelegate.setApplicationLocales(list)
}
Expand Down
73 changes: 73 additions & 0 deletions app/src/main/java/org/scottishtecharmy/soundscape/utils/Locale.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package org.scottishtecharmy.soundscape.utils

import android.app.LocaleManager
import android.content.Context
import android.content.res.Resources
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import org.scottishtecharmy.soundscape.screens.onboarding.language.Language
import java.util.Locale

/**
Expand All @@ -10,3 +15,71 @@ fun getCurrentLocale() : Locale {
val appLocale = AppCompatDelegate.getApplicationLocales()[0]
return appLocale ?: Locale.getDefault()
}

/**
* All languages supported by the app. This is the single source of truth used by both the
* onboarding language picker and the language mismatch detection.
*/
val supportedLanguages = listOf(
Language("العربية المصرية", "arz", "EG"),
Language("中国人", "zh", "CN"),
Language("Dansk", "da", "DK"),
Language("Deutsch", "de", "DE"),
Language("Ελληνικά", "el", "GR"),
Language("English", "en", "US"),
Language("English (UK)", "en", "GB"),
Language("Español", "es", "ES"),
Language("فارسی", "fa", "IR"),
Language("Suomi", "fi", "FI"),
Language("Français (France)", "fr", "FR"),
Language("Français (Canada)", "fr", "CA"),
Language("हिंदी", "hi", "IN"),
Language("Íslenska", "is", "IS"),
Language("Italiano", "it", "IT"),
Language("日本語", "ja", "JP"),
Language("Norsk", "nb", "NO"),
Language("Nederlands", "nl", "NL"),
Language("Polski", "pl", "PL"),
Language("Português (Portugal)", "pt", "PT"),
Language("Português (Brasil)", "pt", "BR"),
Language("Русский", "ru", "RU"),
Language("Română", "ro", "RO"),
Language("Svenska", "sv", "SE"),
Language("українська", "uk", "UK"),
)

/**
* Check if the phone's language differs from the app's configured language and is supported.
* Returns the matching [Language] if the phone language is supported but different from the
* app language, or null if they match or the phone language is not supported.
*/
fun getLanguageMismatch(context: Context): Language? {
val appLocale = AppCompatDelegate.getApplicationLocales()[0]
?: return null // No explicit app locale set — using system default, so no mismatch

// Get the actual device locale. On API 33+, LocaleManager.systemLocales gives the real
// system locales unaffected by per-app locale settings. On older APIs, fall back to
// Resources.getSystem() (which may be affected by AppCompatDelegate on some devices).
val phoneLocale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val localeManager = context.getSystemService(LocaleManager::class.java)
localeManager.systemLocales[0]
} else {
Resources.getSystem().configuration.locales[0]
}

println("phoneLocale $phoneLocale vs appLocale $appLocale")

if (appLocale.language == phoneLocale.language) return null

// Find the best matching supported language for the phone locale
var bestMatch: Language? = null
for (language in supportedLanguages) {
if (language.code == phoneLocale.language && language.region == phoneLocale.country) {
return language // Exact match on language + region
}
if (language.code == phoneLocale.language && bestMatch == null) {
bestMatch = language // Language-only match as fallback
}
}
return bestMatch
}
Loading