diff --git a/android-components/.buildconfig.yml b/android-components/.buildconfig.yml index 1995fd01067b..d50e357367ca 100644 --- a/android-components/.buildconfig.yml +++ b/android-components/.buildconfig.yml @@ -256,6 +256,10 @@ projects: path: components/support/rustlog description: 'A bridge allowing log messages from Rust code to be sent to the log system in support-base' publish: true + support-locale: + path: components/support/locale + description: 'A component to allow apps to change the system defined language by their custom one' + publish: true lib-crash: path: components/lib/crash description: 'A generic crash reporter library that can report crashes to multiple services.' diff --git a/android-components/components/support/base/src/main/res/values/strings.xml b/android-components/components/support/base/src/main/res/values/strings.xml new file mode 100644 index 000000000000..f542bd4e6856 --- /dev/null +++ b/android-components/components/support/base/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + + mozac_support_base_locale_preference_key_locale + diff --git a/android-components/components/support/locale/README.md b/android-components/components/support/locale/README.md new file mode 100644 index 000000000000..87dfee0eb4fb --- /dev/null +++ b/android-components/components/support/locale/README.md @@ -0,0 +1,19 @@ +# [Android Components](../../../README.md) > Support > Locale + +A component to allow apps to change the system defined language by their custom one. + +## Usage + +### Setting up the dependency + +Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)): + +```Groovy +implementation "org.mozilla.components:support-locale:{latest-version}" +``` + +## License + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/ diff --git a/android-components/components/support/locale/build.gradle b/android-components/components/support/locale/build.gradle new file mode 100644 index 000000000000..4ece4c4b10e5 --- /dev/null +++ b/android-components/components/support/locale/build.gradle @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion config.compileSdkVersion + + defaultConfig { + minSdkVersion config.minSdkVersion + targetSdkVersion config.targetSdkVersion + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation Dependencies.kotlin_stdlib + implementation Dependencies.androidx_core + implementation Dependencies.androidx_core_ktx + + implementation project(':support-base') + implementation project(':support-utils') + + testImplementation project(':support-test') + testImplementation Dependencies.androidx_test_core + testImplementation Dependencies.androidx_test_junit + testImplementation Dependencies.testing_robolectric + testImplementation Dependencies.testing_mockito +} + +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/android-components/components/support/locale/gradle.properties b/android-components/components/support/locale/gradle.properties new file mode 100644 index 000000000000..ceb2d0421a06 --- /dev/null +++ b/android-components/components/support/locale/gradle.properties @@ -0,0 +1,2 @@ +# TODO remove +android.enableUnitTestBinaryResources=false diff --git a/android-components/components/support/locale/proguard-rules.pro b/android-components/components/support/locale/proguard-rules.pro new file mode 100644 index 000000000000..f1b424510da5 --- /dev/null +++ b/android-components/components/support/locale/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android-components/components/support/locale/src/main/AndroidManifest.xml b/android-components/components/support/locale/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..d1e7fe4ada3d --- /dev/null +++ b/android-components/components/support/locale/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + diff --git a/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/Extensions.kt b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/Extensions.kt new file mode 100644 index 000000000000..e62cc2b187f8 --- /dev/null +++ b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/Extensions.kt @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.locale + +import java.util.Locale + +fun String.toLocale(): Locale { + val index: Int = if (contains('-')) { + indexOf('-') + } else { + indexOf('_') + } + return if (index != -1) { + val langCode = substring(0, index) + val countryCode = substring(index + 1) + Locale(langCode, countryCode) + } else { + Locale(this) + } +} diff --git a/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareAppCompatActivity.kt b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareAppCompatActivity.kt new file mode 100644 index 000000000000..2c3ef7377051 --- /dev/null +++ b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareAppCompatActivity.kt @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.locale + +import android.content.Context +import androidx.appcompat.app.AppCompatActivity + +/** + * Base activity for apps that want to customized the system defined language by their own. + */ +open class LocaleAwareAppCompatActivity : AppCompatActivity() { + override fun attachBaseContext(base: Context) { + val context = LocaleManager.updateResources(base) + super.attachBaseContext(context) + } +} diff --git a/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareApplication.kt b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareApplication.kt new file mode 100644 index 000000000000..cc565e41cf06 --- /dev/null +++ b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareApplication.kt @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.locale + +import android.app.Application +import android.content.Context +import android.content.res.Configuration + +/** + * Base application for apps that want to customized the system defined language by their own. + */ +open class LocaleAwareApplication : Application() { + + override fun attachBaseContext(base: Context) { + val context = LocaleManager.updateResources(base) + super.attachBaseContext(context) + } + + override fun onConfigurationChanged(config: Configuration) { + super.onConfigurationChanged(config) + LocaleManager.updateResources(this) + } +} diff --git a/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleManager.kt b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleManager.kt new file mode 100644 index 000000000000..28965085578e --- /dev/null +++ b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleManager.kt @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.locale + +import android.content.Context +import android.content.SharedPreferences +import android.content.res.Configuration +import androidx.appcompat.app.AppCompatActivity +import mozilla.components.support.base.R +import mozilla.components.support.base.log.logger.Logger +import java.util.Locale + +/** + * Helper for apps that want to change locale defined by the system. + */ +object LocaleManager { + private val logger = Logger("LocaleManager") + + /** + * Change the system defined locale to the indicated in the [language] parameter. + * This new [language] will be stored and will be the new current locale returned by [getCurrentLocale]. + * + * After calling this function, to visualize the locale changes you have to make sure all your visible activities + * get recreated. If your app is using the single activity approach, this will be trivial just call + * [AppCompatActivity.recreate]. On the other hand, if you have multiple activity this could be tricky, one + * alternative could be restarting your application process see https://github.com/JakeWharton/ProcessPhoenix + * @return A new Context object for whose resources are adjusted to match the new [language]. + */ + fun setNewLocale(context: Context, language: String): Context { + Storage.save(context, language) + return updateResources(context) + } + + /** + * The latest stored locale saved by [setNewLocale]. + */ + fun getCurrentLocale(context: Context): Locale? { + var currentLocale: Locale? = null + + if (currentLocale == null) { + val locale = Storage.getLocale(context) + if (locale != null) { + currentLocale = locale.toLocale() + } + } + return currentLocale + } + + internal fun updateResources(baseContext: Context): Context { + val locale = getCurrentLocale(baseContext) + return if (locale != null) { + updateSystemLocale(locale) + updateConfiguration(baseContext, locale) + } else { + baseContext + } + } + + private fun updateConfiguration(context: Context, locale: Locale): Context { + val configuration = Configuration(context.resources.configuration) + configuration.setLocale(locale) + configuration.setLayoutDirection(locale) + return context.createConfigurationContext(configuration) + } + + private fun updateSystemLocale(locale: Locale) { + Locale.setDefault(locale) + } + + internal fun clear(context: Context) { + Storage.clear(context) + } + + private object Storage { + private const val PREFERENCE_FILE = "mozac_support_base_locale_manager_preference" + private var currentLocal: String? = null + + fun getLocale(context: Context): String? { + return if (currentLocal == null) { + val settings = getSharedPreferences(context) + val key = context.getString(R.string.mozac_support_base_locale_preference_key_locale) + currentLocal = settings.getString(key, null) + currentLocal + } else { + currentLocal + } + } + + @Synchronized + fun save(context: Context, localeCode: String) { + val settings = getSharedPreferences(context) + val key = context.getString(R.string.mozac_support_base_locale_preference_key_locale) + settings.edit().putString(key, localeCode).apply() + currentLocal = localeCode + } + + fun clear(context: Context) { + val settings = getSharedPreferences(context) + settings.edit().clear().apply() + currentLocal = null + } + + private fun getSharedPreferences(context: Context): SharedPreferences { + return context.getSharedPreferences(PREFERENCE_FILE, Context.MODE_PRIVATE) + } + } +} diff --git a/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleManagerTest.kt b/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleManagerTest.kt new file mode 100644 index 000000000000..b5a96803b9a9 --- /dev/null +++ b/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleManagerTest.kt @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.locale + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Locale + +@RunWith(AndroidJUnit4::class) +class LocaleManagerTest { + + @Before + fun setup() { + LocaleManager.clear(testContext) + Locale.setDefault("en_US".toLocale()) + } + + @Test + fun `changing the language to Spanish must change the system locale to Spanish and change the configurations`() { + + var currentLocale = LocaleManager.getCurrentLocale(testContext) + + assertNull(currentLocale) + + val newContext = LocaleManager.setNewLocale(testContext, "es") + + assertNotEquals(testContext, newContext) + + currentLocale = LocaleManager.getCurrentLocale(testContext) + + assertEquals(currentLocale, "es".toLocale()) + assertEquals(currentLocale, Locale.getDefault()) + } + + @Test + fun `when calling updateResources with none current language must not change the system locale neither change configurations`() { + val previousSystemLocale = Locale.getDefault() + val context = LocaleManager.updateResources(testContext) + + assertEquals(testContext, context) + assertEquals(previousSystemLocale, Locale.getDefault()) + } +} \ No newline at end of file