Skip to content

Instantly share code, notes, and snippets.

@ElianFabian
Last active October 29, 2024 14:13
Show Gist options
  • Save ElianFabian/7458f37aca486943087c35eeea352ce0 to your computer and use it in GitHub Desktop.
Save ElianFabian/7458f37aca486943087c35eeea352ce0 to your computer and use it in GitHub Desktop.
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