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 package com.michatec.radio
import android.content.Context
import android.content.res.Configuration
import android.view.Menu import android.view.Menu
import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
import com.michatec.radio.helpers.PreferencesHelper
import java.util.Locale
class ExpandedControllerActivity : ExpandedControllerActivity() { 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 { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.expanded_controller, menu) menuInflater.inflate(R.menu.expanded_controller, menu)
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item) CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
return true 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_REVERB: String = "PRESET_REVERB"
const val PREF_PRESET_DRC: String = "PRESET_DRC" const val PREF_PRESET_DRC: String = "PRESET_DRC"
const val PREF_PRESET_STEREO_WIDTH: String = "PRESET_STEREO_WIDTH" const val PREF_PRESET_STEREO_WIDTH: String = "PRESET_STEREO_WIDTH"
const val PREF_LANGUAGE_SELECTED: String = "PRESET_LANGUAGE_SELECTED"
// default const values // default const values
const val DEFAULT_SIZE_OF_METADATA_HISTORY: Int = 25 const val DEFAULT_SIZE_OF_METADATA_HISTORY: Int = 25
@@ -1,7 +1,9 @@
package com.michatec.radio package com.michatec.radio
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@@ -14,10 +16,13 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.navigateUp import androidx.navigation.ui.navigateUp
import com.michatec.radio.dialogs.LanguageSelectionDialog
import com.michatec.radio.helpers.AppThemeHelper import com.michatec.radio.helpers.AppThemeHelper
import com.michatec.radio.helpers.FileHelper import com.michatec.radio.helpers.FileHelper
import com.michatec.radio.helpers.LanguageHelper
import com.michatec.radio.helpers.PreferencesHelper import com.michatec.radio.helpers.PreferencesHelper
import org.woheller69.freeDroidWarn.FreeDroidWarn import org.woheller69.freeDroidWarn.FreeDroidWarn
import java.util.Locale
/* /*
* MainActivity class * MainActivity class
@@ -27,6 +32,21 @@ class MainActivity : AppCompatActivity() {
/* Main class variables */ /* Main class variables */
private lateinit var appBarConfiguration: AppBarConfiguration 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 */ /* Overrides onCreate from AppCompatActivity */
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
@@ -112,6 +132,9 @@ class MainActivity : AppCompatActivity() {
Keys.PREF_THEME_SELECTION -> { Keys.PREF_THEME_SELECTION -> {
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection()) 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 android.app.Application
import com.michatec.radio.helpers.AppThemeHelper import com.michatec.radio.helpers.AppThemeHelper
import com.michatec.radio.helpers.LanguageHelper
import com.michatec.radio.helpers.PreferencesHelper import com.michatec.radio.helpers.PreferencesHelper
import com.michatec.radio.helpers.PreferencesHelper.initPreferences import com.michatec.radio.helpers.PreferencesHelper.initPreferences
@@ -18,6 +19,7 @@ class Radio : Application() {
initPreferences() initPreferences()
// set Dark / Light theme state // set Dark / Light theme state
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection()) AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
LanguageHelper.setLanguage(this, PreferencesHelper.loadSelectedLanguage())
} }
} }
@@ -17,6 +17,7 @@ import androidx.navigation.fragment.findNavController
import androidx.preference.* import androidx.preference.*
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.michatec.radio.dialogs.ErrorDialog import com.michatec.radio.dialogs.ErrorDialog
import com.michatec.radio.dialogs.LanguageSelectionDialog
import com.michatec.radio.dialogs.PresetSelectionDialog import com.michatec.radio.dialogs.PresetSelectionDialog
import com.michatec.radio.dialogs.ThemeSelectionDialog import com.michatec.radio.dialogs.ThemeSelectionDialog
import com.michatec.radio.dialogs.YesNoDialog import com.michatec.radio.dialogs.YesNoDialog
@@ -31,7 +32,7 @@ import java.util.*
/* /*
* SettingsFragment class * 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 */ /* Define log tag */
@@ -308,6 +309,18 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
return@setOnPreferenceClickListener true 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 // set preference categories
val preferenceCategoryGeneral = PreferenceCategory(activity as Context) val preferenceCategoryGeneral = PreferenceCategory(activity as Context)
@@ -334,6 +347,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
screen.addPreference(preferenceCategoryGeneral) screen.addPreference(preferenceCategoryGeneral)
preferenceCategoryGeneral.addPreference(preferenceThemeSelection) preferenceCategoryGeneral.addPreference(preferenceThemeSelection)
preferenceCategoryGeneral.addPreference(preferenceLanguageSelection)
screen.addPreference(preferenceCategoryAudioEffects) screen.addPreference(preferenceCategoryAudioEffects)
preferenceCategoryAudioEffects.addPreference(preferenceBassBoost) 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 */ /* Updates the enabled/disabled state of EQ controls based on preset selection */
private fun updateEqControlStates() { private fun updateEqControlStates() {
val currentPreset = PreferencesHelper.loadSelectedPreset() 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 */ /* Loads preset Bass Boost */
fun loadPresetBassBoost(): Float { fun loadPresetBassBoost(): Float {
return sharedPreferences.getFloat(Keys.PREF_PRESET_BASS_BOOST, 0f) 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_station_metadata">Currently playing</string>
<string name="player_sheet_h2_stream_url">Streaming link</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 --> <!-- Settings -->
<string name="pref_update_collection_title">Update Stations</string> <string name="pref_update_collection_title">Update Stations</string>
<string name="pref_update_collection_summary">Download latest version of all station.</string> <string name="pref_update_collection_summary">Download latest version of all station.</string>