From 63d85118a4feca4ed8a52f5f86fb59ada1dc7462 Mon Sep 17 00:00:00 2001 From: Michatec Date: Tue, 21 Apr 2026 18:58:53 +0200 Subject: [PATCH] feat(ui): add manual language selection to settings --- .../radio/ExpandedControllerActivity.kt | 26 ++++ app/src/main/java/com/michatec/radio/Keys.kt | 1 + .../java/com/michatec/radio/MainActivity.kt | 23 +++ app/src/main/java/com/michatec/radio/Radio.kt | 2 + .../com/michatec/radio/SettingsFragment.kt | 30 +++- .../radio/dialogs/LanguageSelectionDialog.kt | 140 ++++++++++++++++++ .../michatec/radio/helpers/LanguageHelper.kt | 64 ++++++++ .../radio/helpers/PreferencesHelper.kt | 10 ++ .../main/res/drawable/ic_language_24dp.xml | 9 ++ .../dialog_language_selection.xml | 25 ++++ .../res/layout/dialog_language_selection.xml | 25 ++++ app/src/main/res/values/strings.xml | 15 ++ 12 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/michatec/radio/dialogs/LanguageSelectionDialog.kt create mode 100644 app/src/main/java/com/michatec/radio/helpers/LanguageHelper.kt create mode 100644 app/src/main/res/drawable/ic_language_24dp.xml create mode 100644 app/src/main/res/layout-television/dialog_language_selection.xml create mode 100644 app/src/main/res/layout/dialog_language_selection.xml diff --git a/app/src/main/java/com/michatec/radio/ExpandedControllerActivity.kt b/app/src/main/java/com/michatec/radio/ExpandedControllerActivity.kt index acb7beb..b7ad25b 100644 --- a/app/src/main/java/com/michatec/radio/ExpandedControllerActivity.kt +++ b/app/src/main/java/com/michatec/radio/ExpandedControllerActivity.kt @@ -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) + } + } } diff --git a/app/src/main/java/com/michatec/radio/Keys.kt b/app/src/main/java/com/michatec/radio/Keys.kt index b9c22c9..75c0dd2 100644 --- a/app/src/main/java/com/michatec/radio/Keys.kt +++ b/app/src/main/java/com/michatec/radio/Keys.kt @@ -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 diff --git a/app/src/main/java/com/michatec/radio/MainActivity.kt b/app/src/main/java/com/michatec/radio/MainActivity.kt index ff0091e..ab2911d 100644 --- a/app/src/main/java/com/michatec/radio/MainActivity.kt +++ b/app/src/main/java/com/michatec/radio/MainActivity.kt @@ -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()) + } } } /* diff --git a/app/src/main/java/com/michatec/radio/Radio.kt b/app/src/main/java/com/michatec/radio/Radio.kt index 1579589..2d5f130 100644 --- a/app/src/main/java/com/michatec/radio/Radio.kt +++ b/app/src/main/java/com/michatec/radio/Radio.kt @@ -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()) } } diff --git a/app/src/main/java/com/michatec/radio/SettingsFragment.kt b/app/src/main/java/com/michatec/radio/SettingsFragment.kt index 4946daa..3dce604 100644 --- a/app/src/main/java/com/michatec/radio/SettingsFragment.kt +++ b/app/src/main/java/com/michatec/radio/SettingsFragment.kt @@ -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(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() diff --git a/app/src/main/java/com/michatec/radio/dialogs/LanguageSelectionDialog.kt b/app/src/main/java/com/michatec/radio/dialogs/LanguageSelectionDialog.kt new file mode 100644 index 0000000..cbdc30d --- /dev/null +++ b/app/src/main/java/com/michatec/radio/dialogs/LanguageSelectionDialog.kt @@ -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(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(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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/michatec/radio/helpers/LanguageHelper.kt b/app/src/main/java/com/michatec/radio/helpers/LanguageHelper.kt new file mode 100644 index 0000000..8cfd98b --- /dev/null +++ b/app/src/main/java/com/michatec/radio/helpers/LanguageHelper.kt @@ -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 + } + } +} diff --git a/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt b/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt index dd224d8..03e847e 100644 --- a/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt +++ b/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt @@ -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) diff --git a/app/src/main/res/drawable/ic_language_24dp.xml b/app/src/main/res/drawable/ic_language_24dp.xml new file mode 100644 index 0000000..5fe4b13 --- /dev/null +++ b/app/src/main/res/drawable/ic_language_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout-television/dialog_language_selection.xml b/app/src/main/res/layout-television/dialog_language_selection.xml new file mode 100644 index 0000000..f2f8de9 --- /dev/null +++ b/app/src/main/res/layout-television/dialog_language_selection.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_language_selection.xml b/app/src/main/res/layout/dialog_language_selection.xml new file mode 100644 index 0000000..b56f998 --- /dev/null +++ b/app/src/main/res/layout/dialog_language_selection.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d3fe4ea..a34cc9e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,21 @@ Currently playing Streaming link + + Language + Current language + 🗺️ System + 🇬🇧 English + 🇩🇪 Deutsch + 🇫🇷 Français + 🇷🇺 Русский + 🇯🇵 日本語 + 🇳🇱 Nederlands + 🇵🇱 Polski + 🇬🇷 Ελληνικά + 🇩🇰 Dansk + + Update Stations Download latest version of all station.