Last active
October 29, 2024 14:13
-
-
Save ElianFabian/7458f37aca486943087c35eeea352ce0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package yourpackage | |
import android.annotation.SuppressLint | |
import android.app.Activity | |
import android.app.Application | |
import android.content.Context | |
import android.content.SharedPreferences | |
import android.content.pm.PackageManager | |
import android.content.res.Configuration | |
import android.content.res.Resources | |
import android.os.Build | |
import android.os.Bundle | |
import android.os.LocaleList | |
import android.view.View | |
import android.webkit.WebView | |
import androidx.annotation.RequiresApi | |
import androidx.fragment.app.FragmentActivity | |
import androidx.fragment.app.FragmentManager | |
import androidx.lifecycle.Lifecycle | |
import androidx.navigation.fragment.NavHostFragment | |
import java.util.Locale | |
import java.util.concurrent.CopyOnWriteArrayList | |
// Based on. https://github.com/YarikSOffice/lingver/blob/master/library/src/main/java/com/yariksoffice/lingver/Lingver.kt | |
interface LocaleEasy { | |
val followSystemLocale: Boolean | |
fun followSystemLocale() | |
fun setLocale(locale: Locale) | |
fun setLocaleList(locales: List<Locale>) | |
fun getSystemLocales(): List<Locale> | |
fun getAppLocales(): List<Locale> | |
fun registerFollowSystemLocaleChangeListener(listener: OnFollowSystemLocaleChangeListener) | |
fun unregisterFollowSystemLocaleChangeListener(listener: OnFollowSystemLocaleChangeListener) | |
/** | |
* This interface must be implemented by the all activities that want to be updated when the locale changes. | |
* | |
* We need to be able to recreate the activity view to apply the new locale. | |
* | |
* If your activity does not implement this interface, the activity will be recreated calling [Activity.recreate], | |
* which it's a heavier operation than just recreating the view. | |
*/ | |
interface ViewCreatorComponent { | |
fun createView(): View | |
} | |
fun interface OnFollowSystemLocaleChangeListener { | |
fun onFollowSystemLocaleChange(followSystemLocale: Boolean) | |
} | |
companion object { | |
private var _instance: LocaleEasy? = null | |
@JvmStatic | |
fun getInstance(): LocaleEasy { | |
return _instance ?: throw IllegalStateException("LocaleEasy is not installed. Call LocaleEasy.install() in your application class.") | |
} | |
@JvmStatic | |
fun install(application: Application) { | |
_instance = AndroidLocaleEasy(application) | |
} | |
/** | |
* There's a problem starting from Android 7.0 (API 24) and above, where the WebView doesn't respect the app's locale. | |
* | |
* More info: https://stackoverflow.com/a/59623196/18418162 | |
* | |
* TODO: Test this method. | |
*/ | |
@JvmStatic | |
fun fixWebViewLocale() { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | |
val instance = getInstance() | |
if (instance is AndroidLocaleEasy) { | |
WebView(instance.application).destroy() | |
instance.run { | |
updateResources(application, getAppLocales()) | |
} | |
} | |
} | |
} | |
} | |
} | |
// TODO: add persistence | |
private class AndroidLocaleEasy( | |
val application: Application, | |
) : LocaleEasy, Application.ActivityLifecycleCallbacks { | |
init { | |
application.registerActivityLifecycleCallbacks(this) | |
} | |
private val _preferences = application.getSharedPreferences(LOCALE_SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) | |
private val backstackActivities = CopyOnWriteArrayList<Activity>() | |
private val _followSystemLocaleListeners = CopyOnWriteArrayList<OnFollowSystemLocaleChangeListenerImpl>() | |
override val followSystemLocale: Boolean get() = _followSystemLocale | |
private var _followSystemLocale = _preferences.getBoolean(KEY_FOLLOW_SYSTEM_LOCALE, true) | |
set(value) { | |
_preferences.edit().putBoolean(KEY_FOLLOW_SYSTEM_LOCALE, value).apply() | |
field = value | |
} | |
override fun followSystemLocale() { | |
_followSystemLocale = true | |
} | |
override fun registerFollowSystemLocaleChangeListener(listener: LocaleEasy.OnFollowSystemLocaleChangeListener) { | |
if (_followSystemLocaleListeners.any { it.listener == listener }) { | |
return | |
} | |
val followSystemLocaleChangeListener = OnFollowSystemLocaleChangeListenerImpl(listener) | |
_preferences.registerOnSharedPreferenceChangeListener(followSystemLocaleChangeListener) | |
_followSystemLocaleListeners.add(followSystemLocaleChangeListener) | |
} | |
override fun unregisterFollowSystemLocaleChangeListener(listener: LocaleEasy.OnFollowSystemLocaleChangeListener) { | |
val followSystemLocaleChangeListener = _followSystemLocaleListeners.firstOrNull { it.listener == listener } | |
if (followSystemLocaleChangeListener != null) { | |
_preferences.unregisterOnSharedPreferenceChangeListener(followSystemLocaleChangeListener) | |
_followSystemLocaleListeners.remove(followSystemLocaleChangeListener) | |
} | |
} | |
override fun setLocale(locale: Locale) { | |
setLocaleList(listOf(locale)) | |
} | |
override fun setLocaleList(locales: List<Locale>) { | |
_followSystemLocale = false | |
updateResources(application, locales) | |
refreshStartedActivities(locales) | |
} | |
@Suppress("DEPRECATION") | |
@SuppressLint("ObsoleteSdkInt") | |
override fun getSystemLocales(): List<Locale> { | |
val systemConfiguration = Resources.getSystem().configuration | |
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | |
systemConfiguration.locales.toLocales() | |
} | |
else listOf(systemConfiguration.locale) | |
} | |
@SuppressLint("ObsoleteSdkInt") | |
override fun getAppLocales(): List<Locale> { | |
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | |
LocaleList.getDefault().toLocales() | |
} | |
else listOf(Locale.getDefault()) | |
} | |
private fun refreshStartedActivities(localeList: List<Locale>) { | |
backstackActivities.reversed().forEach { activity -> | |
updateResources(activity, localeList) | |
activity.resetTitle() // TODO: test this | |
if (activity is LocaleEasy.ViewCreatorComponent) { | |
val view = activity.createView() | |
activity.setContentView(view) | |
} | |
else { | |
activity.recreate() | |
} | |
if (activity is FragmentActivity) { | |
refreshStartedFragments(activity.supportFragmentManager) | |
} | |
} | |
} | |
private fun refreshStartedFragments(fragmentManager: FragmentManager) { | |
fragmentManager.fragments.forEach { fragment -> | |
if (!fragment.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && fragment !is NavHostFragment) { | |
return@forEach | |
} | |
try { | |
fragmentManager.beginTransaction() | |
.detach(fragment) | |
.commitNow() | |
fragmentManager.beginTransaction() | |
.attach(fragment) | |
.commitNow() | |
} | |
catch (_: IllegalStateException) { | |
fragmentManager.beginTransaction() | |
.detach(fragment) | |
.commitNowAllowingStateLoss() | |
fragmentManager.beginTransaction() | |
.attach(fragment) | |
.commitNowAllowingStateLoss() | |
} | |
refreshStartedFragments(fragment.childFragmentManager) | |
} | |
} | |
@RequiresApi(Build.VERSION_CODES.N) | |
private fun LocaleList.toLocales(): List<Locale> { | |
if (size() == 0) { | |
return emptyList() | |
} | |
val localeList = this | |
return mutableListOf<Locale>().apply { | |
for (i in 0 until localeList.size()) { | |
add(localeList[i]) | |
} | |
} | |
} | |
@Suppress("DEPRECATION") | |
@SuppressLint("ObsoleteSdkInt") | |
fun updateResources(context: Context, localeList: List<Locale>) { | |
val firstLocale = localeList.first() | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | |
LocaleList.setDefault(LocaleList(*localeList.toTypedArray())) | |
} | |
else { | |
Locale.setDefault(firstLocale) | |
} | |
val resources = context.resources | |
val currentLocale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | |
resources.configuration.locales[0] | |
} | |
else resources.configuration.locale | |
if (currentLocale == firstLocale && localeList.size == 1) { | |
return | |
} | |
val newConfig = Configuration(resources.configuration).apply { | |
when { | |
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> { | |
setLocales(LocaleList(*localeList.toTypedArray())) | |
} | |
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 -> { | |
setLocale(firstLocale) | |
} | |
else -> { | |
this.locale = firstLocale | |
} | |
} | |
} | |
resources.updateConfiguration(newConfig, resources.displayMetrics) | |
} | |
@Suppress("DEPRECATION") | |
private fun Activity.resetTitle() { | |
try { | |
val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | |
packageManager.getActivityInfo(componentName, PackageManager.ComponentInfoFlags.of(PackageManager.GET_META_DATA.toLong())) | |
} | |
else { | |
packageManager.getActivityInfo(componentName, PackageManager.GET_META_DATA) | |
} | |
if (info.labelRes != 0) { | |
setTitle(info.labelRes) | |
} | |
} | |
catch (e: PackageManager.NameNotFoundException) { | |
e.printStackTrace() | |
} | |
} | |
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { | |
backstackActivities.add(activity) | |
} | |
override fun onActivityStarted(activity: Activity) = Unit | |
override fun onActivityResumed(activity: Activity) = Unit | |
override fun onActivityPaused(activity: Activity) = Unit | |
override fun onActivityStopped(activity: Activity) = Unit | |
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit | |
override fun onActivityDestroyed(activity: Activity) { | |
backstackActivities.remove(activity) | |
} | |
private class OnFollowSystemLocaleChangeListenerImpl( | |
val listener: LocaleEasy.OnFollowSystemLocaleChangeListener, | |
) : SharedPreferences.OnSharedPreferenceChangeListener { | |
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { | |
if (key == KEY_FOLLOW_SYSTEM_LOCALE) { | |
listener.onFollowSystemLocaleChange(sharedPreferences?.getBoolean(key, true) ?: true) | |
} | |
} | |
} | |
companion object { | |
const val LOCALE_SHARED_PREFERENCES_NAME = "LocaleEasy" | |
const val KEY_FOLLOW_SYSTEM_LOCALE = "KEY_FOLLOW_SYSTEM_LOCALE" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment