diff --git a/app/src/main/java/org/oppia/android/app/translation/AppLanguageLocaleHandler.kt b/app/src/main/java/org/oppia/android/app/translation/AppLanguageLocaleHandler.kt index 6d3e06581dc..8d317ae34be 100644 --- a/app/src/main/java/org/oppia/android/app/translation/AppLanguageLocaleHandler.kt +++ b/app/src/main/java/org/oppia/android/app/translation/AppLanguageLocaleHandler.kt @@ -23,6 +23,14 @@ class AppLanguageLocaleHandler @Inject constructor( ) { private lateinit var displayLocale: OppiaLocale.DisplayLocale + /** + * Returns whether this handler's tracked locale has been initialized, that is, whether + * [initializeLocale] has been called. + * + * Once this method returns true, it's guaranteed to stay true for the lifetime of this class. + */ + fun isInitialized(): Boolean = ::displayLocale.isInitialized + /** * Initializes this handler with the specified initial [OppiaLocale.DisplayLocale]. * @@ -30,7 +38,7 @@ class AppLanguageLocaleHandler @Inject constructor( * the lifetime of the application. */ fun initializeLocale(locale: OppiaLocale.DisplayLocale) { - check(!::displayLocale.isInitialized) { + check(!isInitialized()) { "Expected to initialize the locale for the first time. If this is in a test, did you use" + " InitializeDefaultLocaleRule?" } @@ -70,7 +78,7 @@ class AppLanguageLocaleHandler @Inject constructor( } private fun verifyDisplayLocaleIsInitialized() { - check(::displayLocale.isInitialized) { + check(isInitialized()) { "Expected locale to be initialized. If this is in a test, did you remember to include" + " InitializeDefaultLocaleRule?" } diff --git a/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt b/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt index b562b744ea2..e811c558b74 100644 --- a/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt +++ b/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt @@ -3,6 +3,7 @@ package org.oppia.android.app.translation import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import org.oppia.android.app.model.ProfileId +import org.oppia.android.domain.locale.LocaleController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult @@ -20,14 +21,46 @@ class AppLanguageWatcherMixin @Inject constructor( private val activity: AppCompatActivity, private val translationController: TranslationController, private val appLanguageLocaleHandler: AppLanguageLocaleHandler, + private val localeController: LocaleController, private val oppiaLogger: OppiaLogger, private val activityRecreator: ActivityRecreator ) { /** * Initializes this mixin by starting language monitoring. This method should only ever be called * once for the lifetime of the current activity. + * + * Note that this method will synchronously ensure that [AppLanguageLocaleHandler] is properly + * initialized if previous bootstrapping was lost (e.g. due to process death), so it must be + * called before interacting with the locale handler to avoid inadvertent crashes in such + * situations. */ fun initialize() { + if (!appLanguageLocaleHandler.isInitialized()) { + /* The handler might have been de-initialized since bootstrapping. This can generally happen + * in two cases: + * 1. Upon crash (later versions of Android will reopen the previous activity rather than + * starting from the launcher activity if the crash occurred with the app in the foreground) + * 2. Upon low-memory process death (the system will restore from a saved instance Bundle of + * the application's activity stack) + * + * In both cases, the locale will be lost & can't be determined until the controller provides + * the state. Since initialization happens during activity initialization, there's no way to + * pass data from a previous instance of the application. Thus, the application can either + * block the main thread on waiting for the data provider result (a strict mode violation that + * could theoretically cause an ANR) or default the locale and, in the event the default is + * wrong, restart the activity after the correct locale is retrieved. For the sake of avoiding + * potential ANRs (even at the potential of perceived jank due to activity recreations), the + * latter option is used here. + */ + oppiaLogger.e( + "AppLanguageWatcherMixin", "Restoring the display locale from de-initialization." + ) + val defaultDisplayLocale = localeController.reconstituteDisplayLocale( + localeController.getLikelyDefaultAppStringLocaleContext() + ) + appLanguageLocaleHandler.initializeLocale(defaultDisplayLocale) + } + // TODO(#52): Hook this up properly to profiles, and handle the non-profile activity cases. val profileId = ProfileId.getDefaultInstance() val appLanguageLocaleDataProvider = translationController.getAppLanguageLocale(profileId) diff --git a/app/src/test/java/org/oppia/android/app/translation/AppLanguageLocaleHandlerTest.kt b/app/src/test/java/org/oppia/android/app/translation/AppLanguageLocaleHandlerTest.kt index f1f96ac7860..70fe77ed24f 100644 --- a/app/src/test/java/org/oppia/android/app/translation/AppLanguageLocaleHandlerTest.kt +++ b/app/src/test/java/org/oppia/android/app/translation/AppLanguageLocaleHandlerTest.kt @@ -242,6 +242,36 @@ class AppLanguageLocaleHandlerTest { assertThat(locales[0].language).isEqualTo("en") } + @Test + fun testIsInitialized_initialState_returnsFalse() { + val isInitialized = appLanguageLocaleHandler.isInitialized() + + // initializeLocale() hasn't yet been called. + assertThat(isInitialized).isFalse() + } + + @Test + fun testIsInitialized_afterInitialization_returnsTrue() { + setAppLanguage(ENGLISH) + appLanguageLocaleHandler.initializeLocale(retrieveAppLanguageLocale()) + + val isInitialized = appLanguageLocaleHandler.isInitialized() + + // The handler should now (& hereafter) be initialized. + assertThat(isInitialized).isTrue() + } + + @Test + fun testIsInitialized_afterInitialization_andUpdate_returnsTrue() { + appLanguageLocaleHandler.initializeLocale(computeNewAppLanguageLocale(ENGLISH)) + appLanguageLocaleHandler.updateLocale(computeNewAppLanguageLocale(BRAZILIAN_PORTUGUESE)) + + val isInitialized = appLanguageLocaleHandler.isInitialized() + + // Updating the locale should keep the handler initialized. + assertThat(isInitialized).isTrue() + } + private fun forceDefaultLocale(locale: Locale) { context.applicationContext.resources.configuration.setLocale(locale) Locale.setDefault(locale) diff --git a/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt b/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt index 24698b37267..b86d62f1f51 100644 --- a/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt +++ b/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt @@ -110,6 +110,9 @@ class AppLanguageWatcherMixinTest { // TODO(#1720): Similar to the above, also add a test to verify that multiple language changes // does not result in multiple recreations for the same activity. It currently will in the test // since two mixins are active, but that won't happen in reality. + // TODO(#1720): Similar to the above, also add 2 tests to verify that mixin initialization in + // cases when the locale isn't initialized (such as process death) prints an error & default + // initializes the locale handler. @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()