feat(ui): add manual language selection to settings

This commit is contained in:
2026-04-21 18:58:53 +02:00
parent 4f150221b7
commit 63d85118a4
12 changed files with 369 additions and 1 deletions
@@ -1,14 +1,40 @@
package com.michatec.radio
import android.content.Context
import android.content.res.Configuration
import android.view.Menu
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
import com.michatec.radio.helpers.PreferencesHelper
import java.util.Locale
class ExpandedControllerActivity : ExpandedControllerActivity() {
override fun attachBaseContext(newBase: Context) {
val languageCode = PreferencesHelper.loadSelectedLanguage()
val context = if (languageCode.isEmpty() || languageCode == "system") {
// Use system default locale
newBase
} else {
val locale = Locale.forLanguageTag(languageCode)
val config = Configuration(newBase.resources.configuration)
config.setLocale(locale)
newBase.createConfigurationContext(config)
}
super.attachBaseContext(context)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.expanded_controller, menu)
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
return true
}
override fun onResume() {
try {
super.onResume()
} catch (_: ClassCastException) {
// Fix for lifecycle exception on some devices (e.g. Samsung)
}
}
}
@@ -91,6 +91,7 @@ object Keys {
const val PREF_PRESET_REVERB: String = "PRESET_REVERB"
const val PREF_PRESET_DRC: String = "PRESET_DRC"
const val PREF_PRESET_STEREO_WIDTH: String = "PRESET_STEREO_WIDTH"
const val PREF_LANGUAGE_SELECTED: String = "PRESET_LANGUAGE_SELECTED"
// default const values
const val DEFAULT_SIZE_OF_METADATA_HISTORY: Int = 25
@@ -1,7 +1,9 @@
package com.michatec.radio
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -14,10 +16,13 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.navigateUp
import com.michatec.radio.dialogs.LanguageSelectionDialog
import com.michatec.radio.helpers.AppThemeHelper
import com.michatec.radio.helpers.FileHelper
import com.michatec.radio.helpers.LanguageHelper
import com.michatec.radio.helpers.PreferencesHelper
import org.woheller69.freeDroidWarn.FreeDroidWarn
import java.util.Locale
/*
* MainActivity class
@@ -27,6 +32,21 @@ class MainActivity : AppCompatActivity() {
/* Main class variables */
private lateinit var appBarConfiguration: AppBarConfiguration
/* Overrides attachBaseContext from AppCompatActivity */
override fun attachBaseContext(newBase: Context) {
val languageCode = PreferencesHelper.loadSelectedLanguage()
val context = if (languageCode.isEmpty() || languageCode == "system") {
// Use system default locale
newBase
} else {
val locale = Locale.forLanguageTag(languageCode)
val config = Configuration(newBase.resources.configuration)
config.setLocale(locale)
newBase.createConfigurationContext(config)
}
super.attachBaseContext(context)
}
/* Overrides onCreate from AppCompatActivity */
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
@@ -112,6 +132,9 @@ class MainActivity : AppCompatActivity() {
Keys.PREF_THEME_SELECTION -> {
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
}
Keys.PREF_LANGUAGE_SELECTED -> {
LanguageHelper.setLanguage(this, PreferencesHelper.loadSelectedLanguage())
}
}
}
/*
@@ -2,6 +2,7 @@ package com.michatec.radio
import android.app.Application
import com.michatec.radio.helpers.AppThemeHelper
import com.michatec.radio.helpers.LanguageHelper
import com.michatec.radio.helpers.PreferencesHelper
import com.michatec.radio.helpers.PreferencesHelper.initPreferences
@@ -18,6 +19,7 @@ class Radio : Application() {
initPreferences()
// set Dark / Light theme state
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
LanguageHelper.setLanguage(this, PreferencesHelper.loadSelectedLanguage())
}
}
@@ -17,6 +17,7 @@ import androidx.navigation.fragment.findNavController
import androidx.preference.*
import com.google.android.material.snackbar.Snackbar
import com.michatec.radio.dialogs.ErrorDialog
import com.michatec.radio.dialogs.LanguageSelectionDialog
import com.michatec.radio.dialogs.PresetSelectionDialog
import com.michatec.radio.dialogs.ThemeSelectionDialog
import com.michatec.radio.dialogs.YesNoDialog
@@ -31,7 +32,7 @@ import java.util.*
/*
* SettingsFragment class
*/
class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener, PresetSelectionDialog.PresetSelectionDialogListener {
class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener, PresetSelectionDialog.PresetSelectionDialogListener, LanguageSelectionDialog.LanguageSelectionDialogListener {
/* Define log tag */
@@ -308,6 +309,18 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
return@setOnPreferenceClickListener true
}
val preferenceLanguageSelection = Preference(context)
preferenceLanguageSelection.title = getString(R.string.pref_language_selection_title)
preferenceLanguageSelection.setIcon(R.drawable.ic_language_24dp)
preferenceLanguageSelection.key = Keys.PREF_LANGUAGE_SELECTED
preferenceLanguageSelection.summary = "${getString(R.string.pref_language_selection_summary)}: ${
LanguageHelper.getCurrentLanguage(activity as Context)
}"
preferenceLanguageSelection.setOnPreferenceClickListener {
LanguageSelectionDialog(this).show(activity as Context)
return@setOnPreferenceClickListener true
}
// set preference categories
val preferenceCategoryGeneral = PreferenceCategory(activity as Context)
@@ -334,6 +347,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
screen.addPreference(preferenceCategoryGeneral)
preferenceCategoryGeneral.addPreference(preferenceThemeSelection)
preferenceCategoryGeneral.addPreference(preferenceLanguageSelection)
screen.addPreference(preferenceCategoryAudioEffects)
preferenceCategoryAudioEffects.addPreference(preferenceBassBoost)
@@ -401,6 +415,20 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
}
}
/* Overrides onLanguageSelectionDialog from LanguageSelectionDialogListener */
override fun onLanguageSelectionDialog(dialogResult: Boolean, selectedLanguage: String) {
if (dialogResult) {
// update summary
val languagePreference = findPreference<Preference>(Keys.PREF_LANGUAGE_SELECTED)
val languageSummary = if (selectedLanguage.isEmpty()) {
getString(R.string.pref_language_system)
} else {
LanguageHelper.getCurrentLanguage(activity as Context)
}
languagePreference?.summary = "${getString(R.string.pref_language_selection_summary)}: $languageSummary"
}
}
/* Updates the enabled/disabled state of EQ controls based on preset selection */
private fun updateEqControlStates() {
val currentPreset = PreferencesHelper.loadSelectedPreset()
@@ -0,0 +1,140 @@
package com.michatec.radio.dialogs
import android.content.Context
import android.view.LayoutInflater
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.michatec.radio.R
import com.michatec.radio.helpers.PreferencesHelper
/*
* LanguageSelectionDialog class
*/
class LanguageSelectionDialog(private var languageSelectionDialogListener: LanguageSelectionDialogListener) {
/* Interface used to communicate back to activity */
interface LanguageSelectionDialogListener {
fun onLanguageSelectionDialog(dialogResult: Boolean, selectedLanguage: String)
}
/* Main class variables */
private lateinit var dialog: AlertDialog
/* Data class representing a supported language */
data class Language(
val code: String,
val nameResId: Int
)
/* List of supported languages - displayed in their own language */
private val supportedLanguages = listOf(
Language("system", R.string.pref_language_system),
Language("en", R.string.pref_language_en),
Language("de", R.string.pref_language_de),
Language("fr", R.string.pref_language_fr),
Language("ru", R.string.pref_language_ru),
Language("ja", R.string.pref_language_ja),
Language("nl", R.string.pref_language_nl),
Language("pl", R.string.pref_language_pl),
Language("el", R.string.pref_language_el),
Language("da", R.string.pref_language_da)
)
/* Counter for generating unique view IDs */
private var viewIdCounter = 0x7F010001 // Starting after android.R.id.home
/* Construct and show dialog */
fun show(context: Context) {
// prepare dialog builder
val builder = MaterialAlertDialogBuilder(context)
// inflate custom layout
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.dialog_language_selection, null)
// find radio group
val radioGroup = view.findViewById<RadioGroup>(R.id.language_radio_group)
val currentLanguage = PreferencesHelper.loadSelectedLanguage()
// add radio buttons for each supported language
for (language in supportedLanguages) {
val radioButton = RadioButton(context).apply {
id = generateViewId()
tag = language.code
text = context.getString(language.nameResId)
textSize = if (isTelevision(context)) 20f else 16f
setPadding(dpToPx(context, 8), dpToPx(context, 16), dpToPx(context, 16), dpToPx(context, 16))
}
radioGroup.addView(radioButton)
}
// set current selection
for (i in 0 until radioGroup.childCount) {
val radioButton = radioGroup.getChildAt(i) as RadioButton
if (radioButton.tag == currentLanguage) {
radioButton.isChecked = true
break
}
}
// if no language is selected, check the first one (system)
if (radioGroup.checkedRadioButtonId == -1) {
val firstButton = radioGroup.getChildAt(0) as RadioButton
firstButton.isChecked = true
}
// set up radio group listener
radioGroup.setOnCheckedChangeListener { _, checkedId ->
val selectedButton = radioGroup.findViewById<RadioButton>(checkedId)
val selectedLanguageCode = selectedButton?.tag as? String ?: "system"
// save language selection to preferences
PreferencesHelper.saveSelectedLanguage(selectedLanguageCode)
// notify listener
languageSelectionDialogListener.onLanguageSelectionDialog(true, selectedLanguageCode)
// dismiss dialog
dialog.dismiss()
}
// set custom view
builder.setView(view)
// handle outside-click as cancel
builder.setOnCancelListener {
languageSelectionDialogListener.onLanguageSelectionDialog(false, "")
}
// display dialog
dialog = builder.create()
dialog.show()
}
/* Generate a unique view ID */
private fun generateViewId(): Int {
return viewIdCounter++
}
/* Helper function to check if device is a TV */
private fun isTelevision(context: Context): Boolean {
val uiMode = context.resources.configuration.uiMode
return (uiMode and android.content.res.Configuration.UI_MODE_TYPE_MASK) == android.content.res.Configuration.UI_MODE_TYPE_TELEVISION
}
/* Helper function to convert dp to pixels */
private fun dpToPx(context: Context, dp: Int): Int {
return (dp * context.resources.displayMetrics.density).toInt()
}
}
@@ -0,0 +1,64 @@
package com.michatec.radio.helpers
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.util.Log
import com.michatec.radio.R
import java.util.Locale
/*
* LanguageHelper object
*/
object LanguageHelper {
/* Define log tag */
private val TAG: String = LanguageHelper::class.java.simpleName
/* Sets the app language on the activity */
fun setLanguage(context: Context, languageCode: String): Boolean {
if (languageCode.isEmpty()) {
Log.i(TAG, "No language code provided, using system default")
return false
}
if (languageCode == "system") {
Log.i(TAG, "Reverting to system default locale")
if (context is Activity) {
context.recreate()
}
return true
}
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
if (context is Activity) {
context.recreate()
}
Log.i(TAG, "Locale changed to: $languageCode")
return true
}
/* Returns a localized resources object */
fun getCurrentLanguage(context: Context): String {
return when (val languageCode = PreferencesHelper.loadSelectedLanguage()) {
"system" -> context.getString(R.string.pref_language_system)
"en" -> context.getString(R.string.pref_language_en)
"de" -> context.getString(R.string.pref_language_de)
"fr" -> context.getString(R.string.pref_language_fr)
"ru" -> context.getString(R.string.pref_language_ru)
"ja" -> context.getString(R.string.pref_language_ja)
"nl" -> context.getString(R.string.pref_language_nl)
"pl" -> context.getString(R.string.pref_language_pl)
"el" -> context.getString(R.string.pref_language_el)
"da" -> context.getString(R.string.pref_language_da)
else -> languageCode
}
}
}
@@ -316,6 +316,16 @@ object PreferencesHelper {
}
}
/* Loads selected language */
fun loadSelectedLanguage(): String {
return sharedPreferences.getString(Keys.PREF_LANGUAGE_SELECTED, "system") ?: "system"
}
/* Saves selected language */
fun saveSelectedLanguage(language: String) {
sharedPreferences.edit { putString(Keys.PREF_LANGUAGE_SELECTED, language) }
}
/* Loads preset Bass Boost */
fun loadPresetBassBoost(): Float {
return sharedPreferences.getFloat(Keys.PREF_PRESET_BASS_BOOST, 0f)
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@color/icon_default"
android:pathData="M280,680L560,680L560,600L280,600L280,680ZM280,520L680,520L680,440L280,440L280,520ZM280,360L680,360L680,280L280,280L280,360ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z" />
</vector>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_language_selection_title"
android:textSize="24sp"
android:textStyle="bold"
android:paddingBottom="16dp" />
<RadioGroup
android:id="@+id/language_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</RadioGroup>
</LinearLayout>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_language_selection_title"
android:textSize="18sp"
android:textStyle="bold"
android:paddingBottom="12dp" />
<RadioGroup
android:id="@+id/language_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</RadioGroup>
</LinearLayout>
+15
View File
@@ -58,6 +58,21 @@
<string name="player_sheet_h2_station_metadata">Currently playing</string>
<string name="player_sheet_h2_stream_url">Streaming link</string>
<!-- Language Selection -->
<string name="pref_language_selection_title">Language</string>
<string name="pref_language_selection_summary">Current language</string>
<string name="pref_language_system">🗺️ System</string>
<string name="pref_language_en" translatable="false">🇬🇧 English</string>
<string name="pref_language_de" translatable="false">🇩🇪 Deutsch</string>
<string name="pref_language_fr" translatable="false">🇫🇷 Français</string>
<string name="pref_language_ru" translatable="false">🇷🇺 Русский</string>
<string name="pref_language_ja" translatable="false">🇯🇵 日本語</string>
<string name="pref_language_nl" translatable="false">🇳🇱 Nederlands</string>
<string name="pref_language_pl" translatable="false">🇵🇱 Polski</string>
<string name="pref_language_el" translatable="false">🇬🇷 Ελληνικά</string>
<string name="pref_language_da" translatable="false">🇩🇰 Dansk</string>
<!-- Settings -->
<string name="pref_update_collection_title">Update Stations</string>
<string name="pref_update_collection_summary">Download latest version of all station.</string>